Keep learning, keep living...

0%

Libvirt和QEMU hook机制介绍

在我们的QEMU/KVM虚拟化环境中,当所有的虚拟机启动时需要自动添加一个ivshmem设备,用于虚拟机与宿主机之间通信。为了添加该设备,我们需要在调用QEMU时,添加上ivshmem设备的相关参数,例如:

1
-device ivshmem,shm=fg_i3,size=8m,bus=pci.0,addr=0x1f

Libvirt使用XML文件来定义虚拟机配置,并根据XML文件来生成QEMU命令行参数,进而执行QEMU程序来启动虚拟机实例。我们可以在所有虚拟机的XML文件的<devices>节点中添加上<shmem>配置,如:

1
2
3
4
5
<shmem name="fg_i3">
<model type="ivshmem" />
<size unit='M'>8</size>
<address type='pci' domain='0x0000' bus='0x00' slot='0x1f' function='0x0' />
</shmem>

这样,libvirt启用QEMU实例时,则会添加如下参数:

1
-device ivshmem,id=shmem0,size=8m,shm=fg_i3,bus=pci.0,addr=0x1f

Guest启动后,登录查看PCI设备,可以看到相应的ivshmem设备:

Libvirt也支持在XML文件中直接定义要添加到QEMU命令行的参数,可以在<domain>节点中,添加<qemu:commandline>来直接添加命令行选项,需要注意的是,要在<domain>中添加命名空间属性: xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0’, 以这种方式同样实现上述逻辑,XML文件如下:

1
2
3
4
5
6
7
8
<domain type="kvm" xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
<name>i3</name>

<qemu:commandline>
<qemu:arg value='-device' />
<qemu:arg value='ivshmem,shm=fg_i3,size=8m,bus=pci.0,addr=0x1f' />
</qemu:commandline>
</domain>

一些场景下,虚拟机XML是由虚拟化平台或云平台动态生成,可能我们修改后又被覆盖回去。修改XML文件这种方式可能不再适用,我们需要寻找另外的方法。很直接的方式就是修改libvirt源码,这里使用的是libvirt-2.0.0版本。

Libvirt中使用qemuBuildCommandLine()函数来生成QEMU命令行,该函数位于src/qemu/qemu_command.c, 该文件中的函数:

1
2
3
4
char *
qemuBuildShmemDevStr(virDomainDefPtr def,
virDomainShmemDefPtr shmem,
virQEMUCapsPtr qemuCaps)

就是用于生成ivshmem相关命令行参数。但这里我们不调用它,可以直接将相应参数字符串,添加进命令行中, 示意代码如下:

1
2
3
4
5
6
7
8
char ivshmem_device[1024];
snprintf(ivshmem_device, 1024,
"ivshmem,shm=fg_%s,size=8m,bus=pci.0,addr=0x1f", uuid);
virCommandAddArgList(cmd, "-device", ivshmem_device, NULL);

if (virQEMUCapsGet(qemuCaps, QEMU_CAPS_MSG_TIMESTAMP) &&
cfg->logTimestamp)
virCommandAddArgList(cmd, "-msg", "timestamp=on", NULL);

如果libvirt是由其他厂商所开发,防止厂商对于libvirt做过修改,我们并不能直接修改代码后替换。这种情况下,我们只能使用更为灵活的HOOK方式。

Libvirt本身支持HOOK机制。虚拟机启动前, libvirt会调用文件$SYSCONFDIR/libvirt/hooks/qemu,在RHEL或CentOS上,一般为/etc/libvirt/hooks/qemulibvirt会将XML文件做为标准输入,虚拟机名称和其他一些参数以标准参数传入,如:

1
/etc/libvirt/hooks/qemu fg_i3 start begin -

我们可以在这个HOOK脚本中为虚拟机准备外部资源,如开放相应的VNC端口,建立iptables规则等等。不过,这个HOOK点并不支持修改libvirt所使用的XML文件,具体参考:
https://libvirt.org/hooks.html

RHEV(Red Hat Enterprise Virtualization)oVirt虚拟化平台中的VDSM则支持启动虚拟机实例前修改XML,这里不详细介绍,具体可以参考:
https://access.redhat.com/documentation/zh-cn/red_hat_enterprise_virtualization/3.6/html/administration_guide/appe-vdsm_and_hooks

Libvirt的HOOK机制不能满足我们的需求,我们可以有另外两种HOOK方式。一种是将原有QEMU二进制文件重命名,我们自己生成一个与原来同名的QEMU wrapper程序,在我们的wrapper程序中,添加参数后再调用原生QEMU,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/python
import sys
import subprocess

_internal = "/usr/libexec/qemu-kvm.bak"

if len(sys.argv) == 1:
sys.exit(subprocess.call(_internal, shell=True))

cmd = _internal + " " + ' '.join(sys.argv[1:])

i = 0
for arg in sys.argv:
if arg == '-uuid' and i < len(sys.argv):
uuid = sys.argv[i + 1]
cmd += " -device ivshmem,shm=fg_%s,size=8,bus=pci.0,addr=0x1f" % uuid
break
i = i + 1

sys.exit(subprocess.call(cmd, shell=True))

另外一种方式则是直接Hook libc中的execve调用。libvirt执行QEMU程序时,最终是使用execve来调用QEMU命令行的,我们可以生成我们自己的execve函数,基于LD_PRELOAD机制来覆盖libc中的execve, 在其中添加参数后,再调用libc中的execve, 示例代码如下:

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
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <dlfcn.h>

typedef ssize_t (*execve_func_t)(const char* filename, char* const argv[], char* const envp[]);

static execve_func_t old_execve = NULL;

int execve(const char* filename, char* const argv[], char* const envp[]) {
char **new_argv;
int i, len;
char device[1024];

old_execve = dlsym(RTLD_NEXT, "execve");

if (strstr(filename, "qemu") == NULL && strstr(filename, "kvm") == NULL) {
return old_execve(filename, argv, envp);
}

for (i = 0; argv[i]; i++) {}

new_argv = malloc(sizeof(char *) * (i + 2));
if (new_argv == NULL) {
return old_execve(filename, argv, envp);
}

device[0] = '\0';
for (i = 0; argv[i]; i++) {
new_argv[i] = argv[i];
if ((strcmp(argv[i], "-uuid") == 0) && (argv[i + 1] != '\0')) {
snprintf(device, 1024,
"ivshmem,shm=fg_%s,size=8m,bus=pci.0,addr=0x1f",
argv[i + 1]);
}
}

if (device[0] == '\0') {
return old_execve(filename, argv, envp);
}

new_argv[i] = "-device";
new_argv[i + 1] = device;
new_argv[i + 2] = NULL;

return old_execve(filename, new_argv, envp);
}

我们将代码编译为so:

1
gcc -fPIC -shared -o demo.so demo.c -ldl

使用LD_PRELOAD加载demo.so, 重新启用libvirtd, 此时再启动Guest,可以看到ivshmem设备参数也已经添加上。

OpenStack的计算结点上,还可以通过修改nova/virt/libvirt/driver.py文件来实现, 其中的_get_guest_xml函数返回实例的XML文件, Newton版本的代码如下:

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
def _get_guest_xml(self, context, instance, network_info, disk_info,
image_meta, rescue=None,
block_device_info=None):
# NOTE(danms): Stringifying a NetworkInfo will take a lock. Do
# this ahead of time so that we don't acquire it while also
# holding the logging lock.
network_info_str = str(network_info)
msg = ('Start _get_guest_xml '
'network_info=%(network_info)s '
'disk_info=%(disk_info)s '
'image_meta=%(image_meta)s rescue=%(rescue)s '
'block_device_info=%(block_device_info)s' %
{'network_info': network_info_str, 'disk_info': disk_info,
'image_meta': image_meta, 'rescue': rescue,
'block_device_info': block_device_info})
# NOTE(mriedem): block_device_info can contain auth_password so we
# need to sanitize the password in the message.
LOG.debug(strutils.mask_password(msg), instance=instance)
conf = self._get_guest_config(instance, network_info, image_meta,
disk_info, rescue, block_device_info,
context)
xml = conf.to_xml()

LOG.debug('End _get_guest_xml xml=%(xml)s',
{'xml': xml}, instance=instance)
return xml

我们可以在函数返回前对XML内容进行修改, 在xml = conf.to_xml()添加修改语句:

1
2
3
4
5
rs = "<qemu:commandline><qemu:arg value='-device'/><qemu:arg value='ivshmem,shm=fg-%s,size=8,bus=pci.0,addr=0x1f'/></qemu:commandline>" % (instance.uuid)
xml = xml.replace('</domain>', rs + '</domain>')
rs1 = "type=\\"kvm\\""
rs2 = "type=\\"kvm\\" xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0\'";
xml = xml.replace(rs1, rs2)

这样返回的XML内容中就含有了需要传给QEMU程序的参数。