mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
Edits that redefine a domain read the wrong XML: - boot menu/order, vCPU hotplug and QoS read XMLDesc(0), which has no secrets, so every such edit, even on a stopped VM, removed the VNC password from the definition; - console, video, CPU, memory and title/description edits read the live XML, so on a running VM pending changes (e.g. a new memory size) were reverted and runtime state was written into the definition. All of them now read VIR_DOMAIN_XML_INACTIVE | SECURE (PERSISTENT_XML). The console setters take the first graphics device of that definition instead of looking it up by the live type. Disk resize no longer redefines the domain. A running VM's image files grow through blockResize, since qemu-img cannot take the lock QEMU holds; block volumes (e.g. LVM) are grown by the storage pool first and QEMU is then told the new size. Live tests reproduce the losses on a RHEL qemu-kvm host; unit tests check the XML flags, the console setters and the resize paths in CI.
127 lines
3.9 KiB
Python
127 lines
3.9 KiB
Python
"""Safety helpers for the live libvirt test suite.
|
|
|
|
Everything the suite creates is named with PREFIX, and its volumes live
|
|
in a dedicated pool. Cleanup only removes prefixed domains and the
|
|
volumes of that pool, so a broken test cannot delete other VMs or images
|
|
on a shared host. inventory() lets a suite check that nothing else
|
|
changed.
|
|
"""
|
|
|
|
import os
|
|
|
|
import libvirt
|
|
|
|
PREFIX = "wvc-test-"
|
|
POOL = "wvc-test"
|
|
POOL_PATH = "/var/lib/libvirt/wvc-test"
|
|
|
|
UNDEFINE_FLAGS = (
|
|
libvirt.VIR_DOMAIN_UNDEFINE_MANAGED_SAVE
|
|
| libvirt.VIR_DOMAIN_UNDEFINE_SNAPSHOTS_METADATA
|
|
| libvirt.VIR_DOMAIN_UNDEFINE_NVRAM
|
|
| libvirt.VIR_DOMAIN_UNDEFINE_CHECKPOINTS_METADATA
|
|
)
|
|
|
|
|
|
def enabled():
|
|
"""The suite creates and deletes VMs, so it only runs on an explicitly named host."""
|
|
return bool(os.environ.get("TEST_LIBVIRT_HOST"))
|
|
|
|
|
|
def ensure_pool(conn):
|
|
try:
|
|
pool = conn.storagePoolLookupByName(POOL)
|
|
except libvirt.libvirtError:
|
|
pool = conn.storagePoolDefineXML(
|
|
f"<pool type='dir'><name>{POOL}</name><target><path>{POOL_PATH}</path></target></pool>"
|
|
)
|
|
pool.build(0)
|
|
if not pool.isActive():
|
|
pool.create(0)
|
|
pool.refresh(0)
|
|
return pool
|
|
|
|
|
|
def cleanup(conn):
|
|
"""Remove prefixed domains and every volume of the test pool."""
|
|
for dom in conn.listAllDomains():
|
|
if not dom.name().startswith(PREFIX):
|
|
continue
|
|
persistent = dom.isPersistent()
|
|
if dom.isActive():
|
|
dom.destroy()
|
|
if persistent:
|
|
dom.undefineFlags(UNDEFINE_FLAGS)
|
|
try:
|
|
pool = conn.storagePoolLookupByName(POOL)
|
|
except libvirt.libvirtError:
|
|
return
|
|
if pool.isActive():
|
|
pool.refresh(0)
|
|
for vol in pool.listAllVolumes():
|
|
vol.delete(0)
|
|
|
|
|
|
def remove_pool(conn):
|
|
cleanup(conn)
|
|
try:
|
|
pool = conn.storagePoolLookupByName(POOL)
|
|
except libvirt.libvirtError:
|
|
return
|
|
if pool.isActive():
|
|
pool.destroy()
|
|
pool.delete(0)
|
|
pool.undefine()
|
|
|
|
|
|
def inventory(conn):
|
|
"""Domains and volumes outside the test namespace."""
|
|
domains = {(d.name(), d.UUIDString()) for d in conn.listAllDomains() if not d.name().startswith(PREFIX)}
|
|
volumes = set()
|
|
for pool in conn.listAllStoragePools():
|
|
if pool.name() == POOL or not pool.isActive():
|
|
continue
|
|
pool.refresh(0)
|
|
volumes.update((pool.name(), v.name()) for v in pool.listAllVolumes())
|
|
return domains, volumes
|
|
|
|
|
|
def create_volume(conn, name, size_mib=64):
|
|
"""A qcow2 volume in the test pool; returns its path."""
|
|
assert name.startswith(PREFIX)
|
|
pool = conn.storagePoolLookupByName(POOL)
|
|
vol = pool.createXML(
|
|
f"<volume><name>{name}.qcow2</name><capacity unit='MiB'>{size_mib}</capacity>"
|
|
"<target><format type='qcow2'/></target></volume>",
|
|
0,
|
|
)
|
|
return vol.path()
|
|
|
|
|
|
OVMF_CODE = "/usr/share/edk2/ovmf/OVMF_CODE.fd"
|
|
|
|
|
|
def define_vm(conn, name, disk_paths, uefi=False, extra_devices=""):
|
|
"""A small q35 VM without an OS; disks are attached as vda, vdb, ..."""
|
|
assert name.startswith(PREFIX)
|
|
disks = "".join(
|
|
f"<disk type='file' device='disk'><driver name='qemu' type='qcow2'/>"
|
|
f"<source file='{path}'/><target dev='vd{chr(ord('a') + i)}' bus='virtio'/></disk>"
|
|
for i, path in enumerate(disk_paths)
|
|
)
|
|
firmware = (
|
|
"<os><type arch='x86_64' machine='q35'>hvm</type>"
|
|
f"<loader readonly='yes' type='pflash'>{OVMF_CODE}</loader></os>"
|
|
if uefi
|
|
else "<os><type arch='x86_64' machine='q35'>hvm</type></os>"
|
|
)
|
|
return conn.defineXML(
|
|
f"<domain type='kvm'><name>{name}</name><memory unit='MiB'>128</memory><vcpu>1</vcpu>"
|
|
f"{firmware}<features><acpi/></features><devices>{disks}{extra_devices}</devices></domain>"
|
|
)
|
|
|
|
|
|
def volume_exists(conn, path):
|
|
pool = conn.storagePoolLookupByName(POOL)
|
|
pool.refresh(0)
|
|
return os.path.basename(path) in pool.listVolumes()
|