libvirt channel + cloud-init 探测虚拟机就绪

Jun 16, 2023 23:00 · 844 words · 2 minute read Virtualization Linux Golang

我们通常可以通过 qemu-guest-agent(qga)与 libvirtd 建立通信来判断虚拟机 Guest OS 是否启动成功(就绪)。观察下面 libvird domain:

<channel type='unix'>
    <source mode='bind' path='/var/lib/libvirt/qemu/channel/target/domain-17-demo_ecs-test0/org.qemu.guest_agent.0'/>
    <target type='virtio' name='org.qemu.guest_agent.0' state='connected'/>
    <alias name='channel0'/>
    <address type='virtio-serial' controller='0' bus='0' port='1'/>
</channel>

定义了一个通道,通过它可以在宿主机和虚拟机之间通信。通道在宿主机上的表现形式为 USock

$ ls -al /var/lib/libvirt/qemu/channel/target/domain-17-demo_ecs-test0/org.qemu.guest_agent.0
srwxrwxr-x 1 qemu qemu 0 Jun 16 15:59 /var/lib/libvirt/qemu/channel/target/domain-17-demo_ecs-test0/org.qemu.guest_agent.0

在虚拟机中的表现形式为虚拟串口(Linux 操作系统中是一个字符设备):

$ ls -al /dev/virtio-ports/org.qemu.guest_agent.0
lrwxrwxrwx. 1 root root 11 Jun 16 15:56 /dev/virtio-ports/org.qemu.guest_agent.0 -> ../vport2p1
$ ls -al /dev/vport2p1
crw-------. 1 root root 243, 1 Jun 16 15:56 /dev/vport2p1

软链接指向了 /dev/vport2p1,Guest OS 中的 qga 打开该设备使用与宿主机上的 libvirtd 建立通信:

$ ps -ef | grep qemu-ga
root         863       1  0 15:56 ?        00:00:00 /usr/bin/qemu-ga --method=virtio-serial --path=/dev/virtio-ports/org.qemu.guest_agent.0 --blacklist=guest-file-open,guest-file-close,guest-file-read,guest-file-write,guest-file-seek,guest-file-flush,guest-exec,guest-exec-status -F/etc/qemu-ga/fsfreeze-hook

如果 domain 中 qga 所使用的通道状态为 connected,说明 qga 已成功运行,就能间接反映 Guest OS 已经就绪。

但由于并非所有虚拟机镜像中都携带 qga,需要寻找一个更通用的方法:利用 libvirt 通道,通过 cloud-init 在实例启动时向 Guest OS 内的虚拟串口发送消息。domain 中 channel 的定义:

<channel type='unix'>
    <source mode='bind' path='/var/lib/libvirt/qemu/channel/target/domain-17-demo_ecs-test0/org.qemu.guest_agent.0'/>
    <target type='virtio' name='org.qemu.guest_agent.0' state='connected'/>
    <alias name='channel0'/>
    <address type='virtio-serial' controller='0' bus='0' port='1'/>
</channel>
<channel type='unix'>
    <source mode='bind' path='/tmp/guestfwd'/>
    <target type='virtio' name='org.qemu.test.0' state='disconnected'/>
    <address type='virtio-serial' controller='0' bus='0' port='2'/>
</channel>

同样地在 domain 启动后,libvirt 会在宿主机上创建 /tmp/guestfwd USock 文件,提供通信的另一端:

$ $ stat /tmp/guestfwd
  File: /tmp/guestfwd
  Size: 0                 Blocks: 0          IO Block: 4096   socket
Device: 801h/2049d        Inode: 4660555     Links: 1
Access: (0775/srwxrwxr-x)  Uid: (  107/    qemu)   Gid: (  107/    qemu)
Access: 2023-03-01 16:30:52.194985624 +0800
Modify: 2023-03-01 16:29:49.375224781 +0800
Change: 2023-03-01 16:29:49.437224545 +0800
 Birth: 2023-03-01 16:29:49.375224781 +0800

在宿主机端我们可以编写程序打开 /tmp/guestfwd 文件通过 IPC 通信的方式与 Guest OS 建立连接,监听从 Guest OS 中发来的消息 hello。下面提供 Go 代码示例:

package main

import (
    "bufio"
    "errors"
    "fmt"
    "io"
    "log"
    "net"
)

func main() {
    conn, err := net.Dial("unix", "/tmp/guestfwd")
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    done := make(chan struct{}, 1)
    go poll(conn, done)

    <-done
}

func poll(conn net.Conn, done chan struct{}) {
    defer close(done)
    reader := bufio.NewReader(conn)
    for {
        data, _, err := reader.ReadLine()
        if errors.Is(err, net.ErrClosed) || errors.Is(err, io.EOF) {
            fmt.Println("connection closed")
            return
        } else if err != nil {
            continue
        }

        fmt.Printf("received %s from channel\n", data)
    }
}

在 Guest OS 端,定义 cloud-init 在操作系统启动时执行 echo hello > /dev/virtio-ports/org.qemu.test.0 命令:

#cloud-config
bootcmd:
- echo hello > /dev/virtio-ports/org.qemu.ecs.0

当宿主机的进程上监听到 hello 字符串,说明 Guest OS 中 cloud-init 已经指向完成,同样间接反映启动成功。

但该方案也有不足之处,要求虚拟机操作系统镜像必须有 cloud-init,而且在 Windows 实例中通过虚拟串口发送文本需要编写 PowerShell 脚本甚至开发 .NET 程序来实现,比 Linux 的一条 echo 命令复杂很多。


挖个坑,准备开始写一个面向 libvirt 编程系列,分享如何从头去实现一个分布式云平台。