| 1 | #!/usr/bin/env python3 |
| 2 | # |
| 3 | # Copyright (c) 2025 Software Freedom Conservancy, Inc. |
| 4 | # |
| 5 | # Author: Yodel Eldar <yodel.eldar@yodel.dev> |
| 6 | # |
| 7 | # SPDX-License-Identifier: GPL-2.0-or-later |
| 8 | """ |
| 9 | Test vhost-user-bridge (vubr) functionality: |
| 10 | |
| 11 | 1) Run vhost-user-bridge on the host. |
| 12 | 2) Launch a guest VM: |
| 13 | a) Instantiate a unix domain socket to the vubr-created path |
| 14 | b) Instantiate a vhost-user backend on top of that socket |
| 15 | c) Map a virtio-net-pci device to the vhost-user backend |
| 16 | d) Instantiate a UDP socket backend |
| 17 | e) Instantiate a user-mode net backend |
| 18 | i) Forward an ephemeral port to port 8080 in-guest with hostfwd= |
| 19 | ii) Expose a generated scratch file to the guest with tftp= |
| 20 | f) Hub the UDP and user-mode backends. |
| 21 | 3) Invoke tftp in the guest to download exported scratch file from the host. |
| 22 | 4) Serve a file to the host via http server in the guest. |
| 23 | """ |
| 24 | |
| 25 | import os |
| 26 | import shutil |
| 27 | import subprocess |
| 28 | from qemu_test import Asset, LinuxKernelTest, which |
| 29 | from qemu_test import exec_command_and_wait_for_pattern |
| 30 | from qemu_test import is_readable_executable_file |
| 31 | from qemu_test import wait_for_console_pattern |
| 32 | from qemu_test.ports import Ports |
| 33 | |
| 34 | class VhostUserBridge(LinuxKernelTest): |
| 35 | |
| 36 | ASSET_KERNEL_INITRAMFS = Asset( |
| 37 | "https://github.com/yodel/vhost-user-bridge-test/raw/refs/heads/main/bzImage", |
| 38 | "8860d7aa59434f483542cdf25b42eacae0d4d4aa7ec923af9589d1ad4703d42b") |
| 39 | |
| 40 | HOST_UUID = "ba4c2e39-627f-487d-ae3b-93cc5d783eb8" |
| 41 | HOST_UUID_HSUM = \ |
| 42 | "d2932e34bf6c17b33e7325140b691e27c191d9ac4dfa550f68c09506facb09b9" |
| 43 | |
| 44 | GUEST_UUID = "143d2b21-fdf0-4c5e-a9ef-f35ebbac8945" |
| 45 | GUEST_UUID_HSUM = \ |
| 46 | "14b64203f5cf2afe520f8be0fdfe630aafc1e85d1301f55a0d1681e68881f3a2" |
| 47 | |
| 48 | def configure_vm(self, ud_socket_path, lport, rport, hostfwd_port, tftpdir): |
| 49 | self.require_accelerator("kvm") |
| 50 | self.require_netdev("vhost-user") |
| 51 | self.require_netdev("socket") |
| 52 | self.require_netdev("hubport") |
| 53 | self.require_netdev("user") |
| 54 | self.require_device("virtio-net-pci") |
| 55 | self.set_machine("q35") |
| 56 | self.vm.add_args( |
| 57 | "-cpu", "host", |
| 58 | "-accel", "kvm", |
| 59 | "-append", "printk.time=0 console=ttyS0", |
| 60 | "-smp", "2", |
| 61 | "-m", "128M", |
| 62 | "-object", "memory-backend-memfd,id=mem0," |
| 63 | "size=128M,share=on,prealloc=on", |
| 64 | "-numa", "node,memdev=mem0", |
| 65 | "-chardev", f"socket,id=char0,path={ud_socket_path}", |
| 66 | "-netdev", "vhost-user,id=vhost0,chardev=char0,vhostforce=on", |
| 67 | "-device", "virtio-net-pci,netdev=vhost0", |
| 68 | "-netdev", f"socket,id=udp0,udp=localhost:{lport}," |
| 69 | f"localaddr=localhost:{rport}", |
| 70 | "-netdev", "hubport,id=hub0,hubid=0,netdev=udp0", |
| 71 | "-netdev", f"user,id=user0,tftp={tftpdir}," |
| 72 | f"hostfwd=tcp:127.0.0.1:{hostfwd_port}-:8080", |
| 73 | "-netdev", "hubport,id=hub1,hubid=0,netdev=user0" |
| 74 | ) |
| 75 | |
| 76 | def assemble_vubr_args(self, vubr_path, ud_socket_path, lport, rport): |
| 77 | vubr_args = [] |
| 78 | |
| 79 | if (stdbuf_path := which("stdbuf")) is None: |
| 80 | self.log.info("Could not find stdbuf: vhost-user-bridge " |
| 81 | "log lines may appear out of order") |
| 82 | else: |
| 83 | vubr_args += [stdbuf_path, "-o0", "-e0"] |
| 84 | |
| 85 | vubr_args += [vubr_path, "-u", f"{ud_socket_path}", |
| 86 | "-l", f"127.0.0.1:{lport}", "-r", f"127.0.0.1:{rport}"] |
| 87 | |
| 88 | return vubr_args |
| 89 | |
| 90 | def test_vhost_user_bridge(self): |
| 91 | prompt = "~ # " |
| 92 | host_uuid_filename = "vubr-test-uuid.txt" |
| 93 | guest_uuid_path = "/tmp/uuid.txt" |
| 94 | kernel_path = self.ASSET_KERNEL_INITRAMFS.fetch() |
| 95 | |
| 96 | vubr_path = self.build_file("contrib", "vhost-user-bridge", |
| 97 | "vhost-user-bridge") |
| 98 | if not is_readable_executable_file(vubr_path): |
| 99 | self.skipTest("Could not find a readable and executable " |
| 100 | "vhost-user-bridge") |
| 101 | |
| 102 | vubr_log_path = self.log_file("vhost-user-bridge.log") |
| 103 | self.log.info("For the vhost-user-bridge application log," |
| 104 | f" see: {vubr_log_path}") |
| 105 | |
| 106 | sock_dir = self.socket_dir() |
| 107 | ud_socket_path = os.path.join(sock_dir.name, "vubr-test.sock") |
| 108 | |
| 109 | tftpdir = self.scratch_file("tftp") |
| 110 | shutil.rmtree(tftpdir, ignore_errors=True) |
| 111 | os.mkdir(tftpdir) |
| 112 | host_uuid_path = self.scratch_file("tftp", host_uuid_filename) |
| 113 | with open(host_uuid_path, "w", encoding="utf-8") as host_uuid_file: |
| 114 | host_uuid_file.write(self.HOST_UUID) |
| 115 | |
| 116 | with Ports() as ports: |
| 117 | # pylint: disable=unbalanced-tuple-unpacking |
| 118 | lport, rport, hostfwd_port = ports.find_free_ports(3) |
| 119 | |
| 120 | self.configure_vm(ud_socket_path, lport, rport, hostfwd_port, |
| 121 | tftpdir) |
| 122 | |
| 123 | vubr_args = self.assemble_vubr_args(vubr_path, ud_socket_path, |
| 124 | lport, rport) |
| 125 | |
| 126 | with open(vubr_log_path, "w", encoding="utf-8") as vubr_log, \ |
| 127 | subprocess.Popen(vubr_args, stdin=subprocess.DEVNULL, |
| 128 | stdout=vubr_log, |
| 129 | stderr=subprocess.STDOUT) as vubr_proc: |
| 130 | self.launch_kernel(kernel_path, wait_for=prompt) |
| 131 | |
| 132 | exec_command_and_wait_for_pattern(self, |
| 133 | f"tftp -g -r {host_uuid_filename} 10.0.2.2 ; " |
| 134 | f"sha256sum {host_uuid_filename}", self.HOST_UUID_HSUM) |
| 135 | wait_for_console_pattern(self, prompt) |
| 136 | |
| 137 | exec_command_and_wait_for_pattern(self, |
| 138 | f"echo -n '{self.GUEST_UUID}' > {guest_uuid_path}", prompt) |
| 139 | self.check_http_download(guest_uuid_path, self.GUEST_UUID_HSUM) |
| 140 | wait_for_console_pattern(self, prompt) |
| 141 | |
| 142 | self.vm.shutdown() |
| 143 | vubr_proc.terminate() |
| 144 | vubr_proc.wait() |
| 145 | |
| 146 | if __name__ == '__main__': |
| 147 | LinuxKernelTest.main() |