Just For Coding

Keep learning, keep living …

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程序的参数。