| 1 | #!/usr/bin/env python3 |
| 2 | # |
| 3 | # Copyright (c) 2025 Nutanix, Inc. |
| 4 | # |
| 5 | # Author: |
| 6 | # Mark Cave-Ayland <mark.caveayland@nutanix.com> |
| 7 | # John Levon <john.levon@nutanix.com> |
| 8 | # |
| 9 | # SPDX-License-Identifier: GPL-2.0-or-later |
| 10 | """ |
| 11 | Check basic vfio-user-pci client functionality. The test starts two VMs: |
| 12 | |
| 13 | - the server VM runs the libvfio-user "gpio" example server inside it, |
| 14 | piping vfio-user traffic between a local UNIX socket and a virtio-serial |
| 15 | port. On the host, the virtio-serial port is backed by a local socket. |
| 16 | |
| 17 | - the client VM loads the gpio-pci-idio-16 kernel module, with the |
| 18 | vfio-user client connecting to the above local UNIX socket. |
| 19 | |
| 20 | This way, we don't depend on trying to run a vfio-user server on the host |
| 21 | itself. |
| 22 | |
| 23 | Once both VMs are running, we run some basic configuration on the gpio device |
| 24 | and verify that the server is logging the expected out. As this is consistent |
| 25 | given the same VM images, we just do a simple direct comparison. |
| 26 | """ |
| 27 | |
| 28 | import os |
| 29 | |
| 30 | from qemu_test import Asset |
| 31 | from qemu_test import QemuSystemTest |
| 32 | from qemu_test import exec_command_and_wait_for_pattern |
| 33 | from qemu_test import wait_for_console_pattern |
| 34 | |
| 35 | # Exact output can vary, so we just sample for some expected lines. |
| 36 | EXPECTED_SERVER_LINES = [ |
| 37 | "gpio: adding DMA region [0, 0xc0000) offset=0 flags=0x3", |
| 38 | "gpio: devinfo flags 0x3, num_regions 9, num_irqs 5", |
| 39 | "gpio: region_info[0] offset 0 flags 0 size 0 argsz 32", |
| 40 | "gpio: region_info[1] offset 0 flags 0 size 0 argsz 32", |
| 41 | "gpio: region_info[2] offset 0 flags 0x3 size 256 argsz 32", |
| 42 | "gpio: region_info[3] offset 0 flags 0 size 0 argsz 32", |
| 43 | "gpio: region_info[4] offset 0 flags 0 size 0 argsz 32", |
| 44 | "gpio: region_info[5] offset 0 flags 0 size 0 argsz 32", |
| 45 | "gpio: region_info[7] offset 0 flags 0x3 size 256 argsz 32", |
| 46 | "gpio: region7: read 256 bytes at 0", |
| 47 | "gpio: region7: read 0 from (0x30:4)", |
| 48 | "gpio: cleared EROM", |
| 49 | "gpio: I/O space enabled", |
| 50 | "gpio: memory space enabled", |
| 51 | "gpio: SERR# enabled", |
| 52 | "gpio: region7: wrote 0x103 to (0x4:2)", |
| 53 | "gpio: I/O space enabled", |
| 54 | "gpio: memory space enabled", |
| 55 | ] |
| 56 | |
| 57 | class VfioUserClient(QemuSystemTest): |
| 58 | """vfio-user testing class.""" |
| 59 | |
| 60 | ASSET_REPO = 'https://github.com/mcayland-ntx/libvfio-user-test' |
| 61 | |
| 62 | ASSET_KERNEL = Asset( |
| 63 | f'{ASSET_REPO}/raw/refs/heads/main/images/bzImage', |
| 64 | '40292fa6ce95d516e26bccf5974e138d0db65a6de0bc540cabae060fe9dea605' |
| 65 | ) |
| 66 | |
| 67 | ASSET_ROOTFS = Asset( |
| 68 | f'{ASSET_REPO}/raw/refs/heads/main/images/rootfs.ext2', |
| 69 | 'e1e3abae8aebb8e6e77f08b1c531caeacf46250c94c815655c6bbea59fc3d1c1' |
| 70 | ) |
| 71 | |
| 72 | def __init__(self, *args, **kwargs): |
| 73 | super().__init__(*args, **kwargs) |
| 74 | |
| 75 | self.kernel_path = None |
| 76 | self.rootfs_path = None |
| 77 | |
| 78 | def configure_server_vm_args(self, server_vm, sock_path): |
| 79 | """ |
| 80 | Configuration for the server VM. Set up virtio-serial device backed by |
| 81 | the given socket path. |
| 82 | """ |
| 83 | server_vm.add_args('-kernel', self.kernel_path) |
| 84 | server_vm.add_args('-append', 'console=ttyS0 root=/dev/sda') |
| 85 | server_vm.add_args('-drive', |
| 86 | f"file={self.rootfs_path},if=ide,format=raw,id=drv0") |
| 87 | server_vm.add_args('-snapshot') |
| 88 | server_vm.add_args('-chardev', |
| 89 | f"socket,id=sock0,path={sock_path},telnet=off,server=on,wait=off") |
| 90 | server_vm.add_args('-device', 'virtio-serial') |
| 91 | server_vm.add_args('-device', |
| 92 | 'virtserialport,chardev=sock0,name=org.fedoraproject.port.0') |
| 93 | |
| 94 | def configure_client_vm_args(self, client_vm, sock_path): |
| 95 | """ |
| 96 | Configuration for the client VM. Point the vfio-user-pci device to the |
| 97 | socket path configured above. |
| 98 | """ |
| 99 | |
| 100 | client_vm.add_args('-kernel', self.kernel_path) |
| 101 | client_vm.add_args('-append', 'console=ttyS0 root=/dev/sda') |
| 102 | client_vm.add_args('-drive', |
| 103 | f'file={self.rootfs_path},if=ide,format=raw,id=drv0') |
| 104 | client_vm.add_args('-snapshot') |
| 105 | client_vm.add_args('-device', |
| 106 | '{"driver":"vfio-user-pci",' + |
| 107 | '"socket":{"path": "%s", "type": "unix"}}' % sock_path) |
| 108 | |
| 109 | def setup_vfio_user_pci_server(self, server_vm): |
| 110 | """ |
| 111 | Start the libvfio-user server within the server VM, and arrange |
| 112 | for data to shuttle between its socket and the virtio serial port. |
| 113 | """ |
| 114 | wait_for_console_pattern(self, 'login:', None, server_vm) |
| 115 | exec_command_and_wait_for_pattern(self, 'root', '#', None, server_vm) |
| 116 | |
| 117 | exec_command_and_wait_for_pattern(self, |
| 118 | 'gpio-pci-idio-16 -v /tmp/vfio-user.sock >/var/tmp/gpio.out 2>&1 &', |
| 119 | '#', None, server_vm) |
| 120 | |
| 121 | # wait for libvfio-user socket to appear |
| 122 | while True: |
| 123 | out = exec_command_and_wait_for_pattern(self, |
| 124 | 'ls --color=no /tmp/vfio-user.sock', '#', None, server_vm) |
| 125 | ls_out = out.decode().splitlines()[1].strip() |
| 126 | if ls_out == "/tmp/vfio-user.sock": |
| 127 | break |
| 128 | |
| 129 | exec_command_and_wait_for_pattern(self, |
| 130 | 'socat UNIX-CONNECT:/tmp/vfio-user.sock /dev/vport0p1,ignoreeof ' + |
| 131 | ' &', '#', None, server_vm) |
| 132 | |
| 133 | def test_vfio_user_pci(self): |
| 134 | """Run basic sanity test.""" |
| 135 | |
| 136 | self.set_machine('pc') |
| 137 | self.require_device('virtio-serial') |
| 138 | self.require_device('vfio-user-pci') |
| 139 | |
| 140 | self.kernel_path = self.ASSET_KERNEL.fetch() |
| 141 | self.rootfs_path = self.ASSET_ROOTFS.fetch() |
| 142 | |
| 143 | sock_dir = self.socket_dir() |
| 144 | socket_path = os.path.join(sock_dir.name, 'vfio-user.sock') |
| 145 | |
| 146 | server_vm = self.get_vm(name='server') |
| 147 | server_vm.set_console() |
| 148 | self.configure_server_vm_args(server_vm, socket_path) |
| 149 | |
| 150 | server_vm.launch() |
| 151 | |
| 152 | self.log.debug('starting libvfio-user server') |
| 153 | |
| 154 | self.setup_vfio_user_pci_server(server_vm) |
| 155 | |
| 156 | client_vm = self.get_vm(name="client") |
| 157 | client_vm.set_console() |
| 158 | self.configure_client_vm_args(client_vm, socket_path) |
| 159 | |
| 160 | try: |
| 161 | client_vm.launch() |
| 162 | except: |
| 163 | self.log.error('client VM failed to start, dumping server logs') |
| 164 | exec_command_and_wait_for_pattern(self, 'cat /var/tmp/gpio.out', |
| 165 | '#', None, server_vm) |
| 166 | raise |
| 167 | |
| 168 | self.log.debug('waiting for client VM boot') |
| 169 | |
| 170 | wait_for_console_pattern(self, 'login:', None, client_vm) |
| 171 | exec_command_and_wait_for_pattern(self, 'root', '#', None, client_vm) |
| 172 | |
| 173 | # |
| 174 | # Here, we'd like to actually interact with the gpio device a little |
| 175 | # more as described at: |
| 176 | # |
| 177 | # https://github.com/nutanix/libvfio-user/blob/master/docs/qemu.md |
| 178 | # |
| 179 | # Unfortunately, the buildroot Linux kernel has some undiagnosed issue |
| 180 | # so we don't get /sys/class/gpio. Nonetheless just the basic |
| 181 | # initialization and setup is enough for basic testing of vfio-user. |
| 182 | # |
| 183 | |
| 184 | self.log.debug('collecting libvfio-user server output') |
| 185 | |
| 186 | out = exec_command_and_wait_for_pattern(self, |
| 187 | 'cat /var/tmp/gpio.out', |
| 188 | 'gpio: region2: wrote 0 to (0x1:1)', |
| 189 | None, server_vm) |
| 190 | |
| 191 | gpio_server_out = [s for s in out.decode().splitlines() |
| 192 | if s.startswith("gpio:")] |
| 193 | |
| 194 | for line in EXPECTED_SERVER_LINES: |
| 195 | if line not in gpio_server_out: |
| 196 | self.log.error(f'Missing server debug line: {line}') |
| 197 | self.fail(False) |
| 198 | |
| 199 | |
| 200 | if __name__ == '__main__': |
| 201 | QemuSystemTest.main() |