前言

一直想写一篇判断应用运行环境的文章,偶尔发现InfoQ发布的付广平老师判断虚拟环境的文章,很值得学习,转载在此。
文末附了一段shell代码,用来判断运行环境,要使用root权限执行。

原文地址:https://www.infoq.cn/article/536l*xprudowckitdgm4

sky-and-grassland.jpg

目前裸机(物理机)、虚拟机、容器是云计算提供计算服务的三种主流形式。最近有人问,如何判断一个虚拟 shell 环境到底是物理机、虚拟机还是容器?
更进一步,如果是物理机,这个物理机厂商是什么,虚拟机到底是 KVM 还是 XEN,容器是 Docker 还是 rkt、lxc 等?
更进一步,如果是虚拟机,是否可以判断这个虚拟机是运行在 AWS 还是阿里或者 OpenStack,是否能够获取虚拟机的 UUID、instance-type、vpc-id、安全组等信息?
这有点像我们在开发中经常使用的反射(reflection)机制,通过反射可以知道一个类实例(instance)的类(class)是什么,更进一步可以知道这个类的父类是什么、实现了哪些方法、包含哪些属性等。
以下是我用到的一些方法,仅供参考。

判断容器

目前应该还没有什么方法能够 100%准确判断虚拟环境是否是容器,至少我没有找到相关文献。
如果环境有systemd-detect-virt命令,则可以直接通过systemd-detect-virt -c命令判断,如果输出为none则不是容器,否则会输出容器类型,比如 lxc。目前很少容器里面放systemd的,我见过的就只有 LXD 的ubuntu镜像,因此这种方法适用性不广。
除此之外,可通过其他 tricks 判断,最简便的方法判断 PID 为 1 的进程,如果该进程就是应用进程则判断是容器,而如果是 init 进程或者 systemd 进程,则不一定是容器,当然不能排除是容器的情况,比如 LXD 实例的进程就为/sbin/init。
容器通过 PID namespace 实现与宿主机以及其他容器的进程 PID 隔离,但是所有容器的进程在宿主机是完全可见的,并且 PID 不一样。
容器和虚拟机不一样的是,容器和宿主机是共享内核的,因此理论上容器内部是没有内核文件的,除非挂载了宿主机的/boot目录:

1
2
KERNEL_PATH=$(cat /proc/cmdline | tr ' ' '\n' | awk -F '=' '/BOOT_IMAGE/{print $2}')
test -e $KERNEL_PATH && echo "Not Sure" || echo "Container"

另外,我们知道容器是通过 cgroup 实现资源限制,每个容器都会放到一个 cgroup 组中,如果是 Docker,则 cgroup 的名称为docker-xxxx,其中xxxx为 Docker 容器的 UUID。而控制容器的资源,本质就是控制运行在容器内部的进程资源,因此我们可以通过查看容器内部进程为 1 的 cgroup 名称获取线索。
如下是我通过 Docker 跑 busybox 的 cgroup 信息:

1
2
3
4
5
6
7
8
9
10
11
12
# docker run -t -i --rm busybox cat /proc/1/cgroup
11:memory:/system.slice/docker-9da...11.scope
10:pids:/system.slice/docker-9da...11.scope
9:hugetlb:/system.slice/docker-9da...11.scope
8:blkio:/system.slice/docker-9da...11.scope
7:cpuset:/system.slice/docker-9da...11.scope
6:devices:/system.slice/docker-9da...11.scope
5:perf_event:/system.slice/docker-9da...11.scope
4:freezer:/system.slice/docker-9da...11.scope
3:cpuacct,cpu:/system.slice/docker-9da...11.scope
2:net_prio,net_cls:/system.slice/docker-9da...11.scope
1:name=systemd:/system.slice/docker-9da...11.scope

我们不仅可以知道这是 Docker 容器,还获取了 Docker 容器的 UUID 为9ba…11。
根据如上的结论,判断一个虚拟环境是否 Docker 的脚本为:

1
2
3
cat /proc/1/cgroup | grep -qi docker \
&& echo "Docker" \
|| echo "Not Docker"

当然如果仅仅判断是否 Docker 容器,还能通过判断是否存在.dockerenv文件区分是否 Docker 容器:

1
[[ -f /.dockerenv ]] && echo "Docker" || echo "Not Docker"

rkt 容器类似,输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
# rkt --insecure-options=image run docker://busybox  --exec cat -- '/proc/1/cgroup'
[ 1547.858418] busybox[6]: 11:perf_event:/
[ 1547.858758] busybox[6]: 10:hugetlb:/
[ 1547.859055] busybox[6]: 9:memory:/machine.slice/machine-rkt\x2dd6863650\x2da1fa\x2d4a12\x2db754\x2da4e6641023cd.scope
[ 1547.859309] busybox[6]: 8:cpuset:/
[ 1547.859549] busybox[6]: 7:pids:/machine.slice/machine-rkt\x2dd6863650\x2da1fa\x2d4a12\x2db754\x2da4e6641023cd.scope
[ 1547.859781] busybox[6]: 6:net_cls,net_prio:/
[ 1547.860038] busybox[6]: 5:blkio:/machine.slice/machine-rkt\x2dd6863650\x2da1fa\x2d4a12\x2db754\x2da4e6641023cd.scope
[ 1547.860278] busybox[6]: 4:cpu,cpuacct:/machine.slice/machine-rkt\x2dd6863650\x2da1fa\x2d4a12\x2db754\x2da4e6641023cd.scope
[ 1547.860509] busybox[6]: 3:freezer:/
[ 1547.860756] busybox[6]: 2:devices:/machine.slice/machine-rkt\x2dd6863650\x2da1fa\x2d4a12\x2db754\x2da4e6641023cd.scope
[ 1547.861026] busybox[6]: 1:name=systemd:/machine.slice/machine-rkt\x2dd6863650\x2da1fa\x2d4a12\x2db754\x2da4e6641023cd.scope/init.scope

如上的\x2d为-号:

1
2
3
4
5
# python -c 'print("\x2d".decode())'
-
# echo "machine-rkt\x2dd6863650\x2da1fa\x2d4a12\x2db754\x2da4e6641023cd.scope" \
| sed 's/\\x2d/-/g'
machine-rkt-d6863650-a1fa-4a12-b754-a4e6641023cd.scope

因此判断一个虚拟环境是否 rkt 的脚本为:

1
2
3
4
cat /proc/1/cgroup | \
grep -q 'machine-rkt' \
&& echo 'rkt' \
|| echo 'not rkt'

好奇 AWS lambda 的运行环境是什么,于是写了个函数输出/proc/1/cgroup,结果为:

1
2
3
4
5
6
7
8
9
9:perf_event:/
8:memory:/sandbox-8a8cb2
7:hugetlb:/
6:freezer:/sandbox-55f57b
5:devices:/
4:cpuset:/
3:cpuacct:/sandbox-cf5b48
2:cpu:/sandbox-root-HaLK4d/sandbox-305dc7
1:blkio:/

猜测是一种叫sandbox的运行环境,估计也是一种容器。
判断虚拟环境是否为容器环境相对比较复杂,目前没有完美的方案,总结过程如下:

  • 判断是否可运行systemd-detect-virt -c命令,如果输出为none则不是容器,否则可确定容器类型。
  • 判断PID 1如果为应用本身,则该虚拟环境是容器,否则不能确定是否是容器。
  • 判断是否存在加载的内核文件,如果不存在,则可判断为容器,否则不能确定是否为容器。
  • 判断是否存在/.dockerenv文件,如果存在则为 Docker 容器,否则不能确定是否为容器。
  • 读取/proc/1/cgroup文件,判断是否包含docker、rkt等关键字,如果包含,则说明为容器,否则不能确定是否为容器。

另外,需要特别注意的是,容器必须最先判断,因为容器本身并没有任何的硬件虚拟化,容器看到的硬件特性信息和宿主机看到的完全一样,因此下面介绍的通过lscpu以及 DMI 信息判断是否是虚拟机或者物理机,对容器并不适用。换句话说,不能因为lscpu的Hypervisor vendor值为KVM就说明一定是 KVM 虚拟机,因为它也有可能是容器。下文均假设已经排除为容器的情况。

判断物理机

如果使用了 systemd,则可以直接通过systemd-detect-virt命令判断是否物理机:

1
2
systemd-detect-virt
none

如果输出为none,则说明是物理机。
当然也可根据 lscpu 命令输出,看是否有Hypervisor vendor属性,如果没有该属性,则一般为物理机,如果存在该属性则一定是虚拟机:

1
2
3
lscpu | grep -Piq 'Hypervisor vendor' \
&& echo "Virtual Machine" \
|| echo "Physical Machine"

获取物理机的信息最直接的方式是查看 DMI 信息/sys/firmware/dmi/tables/DMI,使用dmidecode命令解码:

1
2
3
4
5
6
7
8
9
10
11
# dmidecode -t system
System Information
Manufacturer: HP
Product Name: ProLiant DL380 Gen9
Version: Not Specified
Serial Number: 6CU6468KKD
UUID: 30393137-3136-4336-5536-3436384B4B44
Wake-up Type: Power Switch
SKU Number: 719061-B21
Family: ProLiant

如上可以看出这是台物理机,厂商为 HP,型号为 ProLiant DL380 Gen9,序列号为 6CU6468KKD。
通过ipmitool命令可以查看物理服务器的带外 IP:

1
2
3
4
5
ipmitool lan print \
| grep -Pi 'IP Address\s+:' \
| cut -d ':' -f 2 \
| tr -d ' '
192.168.0.35

当然如果是虚拟机,如上命令会执行失败。
另外也可以通过其他命令查看物理信息,如lshw命令。

判断虚拟机

其实前面已经提到了,如果使用了 systemd,则可以直接通过systemd-detect-virt命令判断是否虚拟机:

1
2
systemd-detect-virt

如果是虚拟机,则会输出虚拟机类型,如 kvm、oracle(virtualbox)、xen 等。
当然也可根据 lscpu 命令输出,查看Hypervisor vendor属性值:

1
lscpu | grep -i 'Hypervisor vendor' | cut -d ':' -f 2 | tr -d ' '

通过如上命令,我的一台 AWS 虚拟机输出为Xen,阿里云虚拟机为KVM,VirtualBox 虚拟机也输出为KVM,这是因为我使用了 KVM 硬件加速虚拟化。
我的搬瓦工虚拟机输出也为KVM,可见搬瓦工主机也是 KVM 虚拟机。
通过如上方法可以获取虚拟机的虚拟化类型,能否获取更多信息呢?参考物理机的获取方式,我们可以通过dmidecode命令获取更多的虚拟机信息。比如我在一台 OpenStack 虚拟机运行如下命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# dmidecode -t system
# dmidecode 3.1
Getting SMBIOS data from sysfs.
SMBIOS 2.8 present.

Handle 0x0100, DMI type 1, 27 bytes
System Information
Manufacturer: OpenStack Foundation
Product Name: OpenStack Nova
Version: 15.0.1
Serial Number: 00310981-9899-e411-906e-00163566263e
UUID: 1a48e29f-a023-48b8-b06b-afa63a9cff00
Wake-up Type: Power Switch
SKU Number: Not Specified
Family: Virtual Machine

如上 Manufacturer 为OpenStack Foundation,说明运行在 OpenStack 平台,Version 为 Nova 版本,根据 OpenStack 的releases可知15.0.1对应为 OpenStack Ocata 版本,而 UUID 即虚拟机的 UUID。
AWS 上的一台虚拟机输出为:

1
2
3
4
5
6
7
8
9
10
$ sudo dmidecode -t system
System Information
Manufacturer: Xen
Product Name: HVM domU
Version: 4.2.amazon
Serial Number: ec24ea61-ebbd-d428-e3d0-50ca37b49074
UUID: ec24ea61-ebbd-d428-e3d0-50ca37b49074
Wake-up Type: Power Switch
SKU Number: Not Specified
Family: Not Specified

在 Version 中标明了amazon字样。
阿里云虚拟机如下:

1
2
3
4
5
6
7
8
9
10
# dmidecode -t system
System Information
Manufacturer: Alibaba Cloud
Product Name: Alibaba Cloud ECS
Version: pc-i440fx-2.1
Serial Number: f1099eb2-f7b6-4b8f-a02e-0a004b66dc6a
UUID: F1099EB2-F7B6-4B8F-A02E-0A004B66DC6A
Wake-up Type: Power Switch
SKU Number: Not Specified
Family: Not Specified

可见虽然可以从 system 信息中获取云厂商的线索,但其实虚拟机的 system 信息并没有统一的标准,有的在version中体现,有的在Product Name中表现,完全取决于云厂商自己的配置。

如上整合如下脚本初略判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/bash

detect_cloud_provider()
{
if dmidecode -t system | grep -qi 'amazon'; then
echo "AWS"
return 0
fi

if dmidecode -t system | grep -qi 'openstack'; then
echo "OpenStack"
return 0
fi

if dmidecode -t system | grep -qi 'alibaba'; then
echo "Aliyun"
return 0
fi
# ...
echo "Unknown"
return 1
}

detect_cloud_provider $@

如上也可以判断公有云是否基于 OpenStack 实现,比如华为虚拟机输出为 OpenStack,可大致猜测华为的公有云是基于 OpenStack 实现的。
AWS 以及 OpenStack 系的虚拟机还可以通过 metadata 或者 ConfigDrive 获取更多信息,以 metadata 为例:
获取虚拟机的 ID:

1
2
3
$ curl -L 169.254.169.254/latest/meta-data/instance-id && echo
i-060a8dca9edf35512

获取内网 IP:

1
2
3
$ curl -L 169.254.169.254/latest/meta-data/local-ipv4 && echo
192.168.0.111

获取 instance type(规格):

1
2
$ curl -L 169.254.169.254/latest/meta-data/instance-type && echo
t2.medium

获取虚拟机的公有 IP(弹性 IP),这个挺有用的,因为在虚拟机没法通过ifconfig查看弹性 IP,经常登录虚拟机后,忘记自己的公有 IP:

1
2
3
$ curl -L 169.254.169.254/latest/meta-data/public-ipv4 && echo
52.82.103.54

其他的比如 vpc-id、ami id(镜像 id)、安全组、公钥名等都可以通过该方式获取。
如果是 OpenStack,还可以使用 OpenStack 的 metadata 获取更多信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# curl -sL 169.254.169.254/openstack/latest/meta_data.json \
| python -m json.tool
{
"availability_zone": "nova",
"devices": [],
"hostname": "int32bit-test-1",
"launch_index": 0,
"meta": {
"cinder_img_volume_type": "07c40142-f06e-483c-8c27-2970579e94b1",
"volume_type": "07c40142-f06e-483c-8c27-2970579e94b1"
},
"name": "int32bit-test-1",
"project_id": "ad19a76ab29c4e448d6efc9645369d0e",
"random_seed": "...",
"uuid": "1a48e29f-a023-48b8-b06b-afa63a9cff00"
}

如上可获取虚拟机的租户 ID、volume type 等信息。当然邪恶点可以通过查看 userdata 获取虚拟机初始化 root 密码。AWS 甚至可以查看AccessKeyId以及SecretAccessKey。

总结

如上总结了几种判断虚拟化环境类型的方法,不一定准确,仅供参考,当然也可能还有其他更好的方法。
如下是根据前面的结论写的一个探测虚拟化类型的脚本,不一定健壮完备,仅供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#!/bin/sh

detect_container()
{
if which systemd-detect-virt >/dev/null 2>&1; then
TYPE=$(systemd-detect-virt -c)
if [ "$TYPE" = "none" ]; then
return 1
else
echo "Container: $TYPE"
return 0
fi
fi
if [ -n "$container" ]; then
echo "Container: $container"
return 0
fi
if grep -qi docker /proc/1/cgroup; then
echo "Container: Docker"
return 0
fi
if test -f /.dockerenv; then
echo "Container: Docker"
return 0
fi
if grep -qi 'machine-rkt' /proc/1/cgroup; then
echo "Container: rkt"
return 0
fi
# Other container type detect here
return 1
}

detect_physical()
{
if ! lscpu | grep -qi 'Hypervisor vendor'; then
echo "Physical: $(cat /sys/class/dmi/id/product_name)"
return 0
fi
return 1
}

detect_virtual_machine()
{
if lscpu | grep -qi 'Hypervisor vendor'; then
HYPER_TYPE=$(lscpu | grep -i "Hypervisor vendor" \
| cut -d ':' -f 2 | sed 's/^ *//g')
if dmidecode -t system | grep -qi 'amazon'; then
echo "Virtual Machine: AWS/$HYPER_TYPE"
elif dmidecode -t system | grep -qi 'openstack'; then
echo "Virtual Machine: OpenStack/$HYPER_TYPE"
elif dmidecode -t system | grep -qi 'alibaba'; then
echo "Virtual Machine: Aliyun/$HYPER_TYPE"
else
Manufacturer=$(dmidecode -t system | grep 'Manufacturer' \
| cut -d ':' -f 2 | sed 's/^ *//g')
ProductName=$(dmidecode -t system | grep 'Product Name' \
| cut -d ':' -f 2 | sed 's/^ *//g')
Version=$(dmidecode -t system | grep 'Version' \
| cut -d ':' -f 2 | sed 's/^ *//g')
echo "Virtual Machine: $Manufacturer $ProductName($Version)/$HYPER_TYPE"
fi
return 0
fi
return 1
}

detect_virtual_type()
{
detect_container || detect_physical \
|| detect_virtual_machine || echo "Unknown"
}

detect_virtual_type "$@"