#!/usr/bin/env python3 """Isolated PVE Q35 smoke test: no guest disk, network backend or passed GPU. Runs a paused TCG QEMU process and checks PCI properties and usb-redir hotplug. It never connects to a managed VM's QMP socket or changes PVE configuration. """ import importlib.util import json from pathlib import Path import socket import subprocess import tempfile import time spec = importlib.util.spec_from_file_location('policy', Path(__file__).with_name('set-fixed-pci.py')) policy = importlib.util.module_from_spec(spec) spec.loader.exec_module(policy) def main(): with tempfile.TemporaryDirectory(prefix='vdi-fixed-pci-smoke-') as tmp: path = str(Path(tmp) / 'qmp.sock') flags = [part for value in policy.FIXED_GLOBALS for part in ('-global', value)] cmd = ['/usr/bin/kvm', '-machine', 'pc-q35-11.0+pve0,accel=tcg', '-m', '128', '-nodefaults', '-display', 'none', '-S', '-readconfig', '/usr/share/qemu-server/pve-q35-4.0.cfg', '-qmp', 'unix:' + path + ',server=on,wait=off', *flags, '-device', 'virtio-net-pci,id=diagnet,bus=pci.0,addr=0x12', '-device', 'virtio-serial,id=diagserial,bus=pci.0,addr=0x8', '-device', 'ahci,id=diagahci,bus=pci.0,addr=0x7'] proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) try: deadline = time.monotonic() + 15 while not Path(path).exists(): if proc.poll() is not None: raise RuntimeError(proc.stderr.read().decode()) if time.monotonic() > deadline: raise RuntimeError('QEMU startup timed out') time.sleep(.1) with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: sock.settimeout(5) sock.connect(path) with sock.makefile('rwb', buffering=0) as stream: print('QEMU_VERSION', json.loads(stream.readline())['QMP']['version']) sequence = 0 def call(name, arguments=None): nonlocal sequence sequence += 1 ident = sequence obj = {'execute': name, 'id': ident} if arguments is not None: obj['arguments'] = arguments stream.write(json.dumps(obj).encode() + b'\n') while True: line = stream.readline() if not line: raise RuntimeError('QMP disconnected') obj = json.loads(line) if obj.get('id') == ident: if 'error' in obj: raise RuntimeError(str(obj['error'])) return obj['return'] call('qmp_capabilities') for port in range(1, 5): value = call('qom-get', {'path': f'/machine/peripheral/ich9-pcie-port-{port}', 'property': 'hotplug'}) if value is not False: raise RuntimeError('Root port remains hotpluggable') print('PCIE_PORT_HOTPLUG_OFF: 4/4') for bridge in range(4): value = call('qom-get', {'path': f'/machine/peripheral/pci.{bridge}', 'property': 'shpc'}) if value is not False: raise RuntimeError('SHPC remains enabled') print('PCI_BRIDGE_SHPC_OFF: 4/4') lpc = [p for p in call('qom-list', {'path': '/machine/unattached'}) if p['type'] == 'child'] if len(lpc) != 1: raise RuntimeError('Cannot uniquely identify ICH9 LPC') value = call('qom-get', {'path': '/machine/unattached/' + lpc[0]['name'], 'property': 'acpi-pci-hotplug-with-bridge-support'}) if value is not False: raise RuntimeError('ACPI PCI hotplug remains enabled') print('ACPI_PCI_HOTPLUG_OFF') call('chardev-add', {'id': 'diag-channel', 'backend': {'type': 'null', 'data': {}}}) call('device_add', {'driver': 'usb-redir', 'id': 'diag-usb', 'chardev': 'diag-channel', 'bus': 'ehci-2.0', 'filter': '0x08:-1:-1:-1:1|-1:-1:-1:-1:0'}) if 'diag-usb' not in call('human-monitor-command', {'command-line': 'info usb'}): raise RuntimeError('USB device not attached') print('USB_REDIR_ADD_OK') call('device_del', {'id': 'diag-usb'}) deadline = time.monotonic() + 5 while any(p['name'] == 'diag-usb' for p in call('qom-list', {'path': '/machine/peripheral'})): if time.monotonic() > deadline: raise RuntimeError('USB removal pending') time.sleep(.1) # QEMU 11 can remove the anonymous backend with the device. if any(p['label'] == 'diag-channel' for p in call('query-chardev')): call('chardev-remove', {'id': 'diag-channel'}) print('USB_REDIR_REMOVE_OK') call('quit') proc.wait(timeout=5) print('ISOLATED_SMOKE_PASS (Windows UI and physical USB not tested)') finally: if proc.poll() is None: proc.terminate() proc.wait(timeout=5) print('QEMU_STDERR:', proc.stderr.read().decode()) if __name__ == '__main__': main()