118 lines
6.0 KiB
Python
118 lines
6.0 KiB
Python
#!/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<ICH9-LPC>']
|
|
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()
|