1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-09 11:46:49 +00:00

fix(instances): domain edits start from the persistent definition

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.
This commit is contained in:
catborise 2026-10-04 00:27:55 +03:00
parent 47c480f059
commit 08bf39ef61
6 changed files with 293 additions and 59 deletions

View file

@ -101,7 +101,7 @@ def create_volume(conn, name, size_mib=64):
OVMF_CODE = "/usr/share/edk2/ovmf/OVMF_CODE.fd"
def define_vm(conn, name, disk_paths, uefi=False):
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(
@ -117,7 +117,7 @@ def define_vm(conn, name, disk_paths, uefi=False):
)
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}</devices></domain>"
f"{firmware}<features><acpi/></features><devices>{disks}{extra_devices}</devices></domain>"
)

112
instances/test_live_xml.py Normal file
View file

@ -0,0 +1,112 @@
"""Live checks that persistent edits keep secrets and pending changes (R-01, R-02).
Every edit that redefines the domain must start from the persistent
definition with secrets (VIR_DOMAIN_XML_INACTIVE | VIR_DOMAIN_XML_SECURE).
Runs only with TEST_LIBVIRT_HOST set (see instances/livetest.py).
"""
import os
import unittest
import libvirt
from django.test import SimpleTestCase
from lxml import etree
from vrtManager.connection import connection_manager
from vrtManager.instance import wvmInstance
from . import livetest
P = livetest.PREFIX
MAC = "52:54:00:aa:01:01"
DEVICES = (
f"<interface type='network'><mac address='{MAC}'/><source network='default'/><model type='virtio'/></interface>"
"<graphics type='vnc' autoport='yes' passwd='keep1234'/>"
"<video><model type='vga' primary='yes'/></video>"
)
PERSISTENT = libvirt.VIR_DOMAIN_XML_INACTIVE | libvirt.VIR_DOMAIN_XML_SECURE
# name -> (edit, allowed while running)
EDITS = {
"set_bootmenu": (lambda vm: vm.set_bootmenu(1), True),
"set_bootorder": (lambda vm: vm.set_bootorder({0: {"type": "disk", "dev": "vda"}}), True),
"set_vcpu_hotplug": (lambda vm: vm.set_vcpu_hotplug(True, 1), False),
"set_console_keymap": (lambda vm: vm.set_console_keymap("de"), True),
"set_console_listener_addr": (lambda vm: vm.set_console_listener_addr("127.0.0.1"), True),
"set_video_model": (lambda vm: vm.set_video_model("virtio"), True),
"resize_cpu": (lambda vm: vm.resize_cpu("1", "2"), True),
"resize_mem": (lambda vm: vm.resize_mem(128, 160), False),
"resize_disk": (lambda vm: vm.resize_disk([{"path": vm.get_disk_devices()[0]["path"], "size_new": 96 << 20}]), True),
"set_options": (lambda vm: vm.set_options({"title": "edited"}), True),
"set_qos": (lambda vm: vm.set_qos(MAC, "inbound", 100, 200, 300), True),
"unset_qos": (lambda vm: vm.unset_qos(MAC, "inbound"), True),
}
class LivePersistentEditTestCase(SimpleTestCase):
@classmethod
def setUpClass(cls):
if not livetest.enabled():
raise unittest.SkipTest("Set TEST_LIBVIRT_HOST to run the live libvirt tests")
cls.args = (
os.environ["TEST_LIBVIRT_HOST"],
os.environ.get("TEST_LIBVIRT_LOGIN", ""),
os.environ.get("TEST_LIBVIRT_PASSWORD", ""),
int(os.environ.get("TEST_LIBVIRT_TYPE", 4)),
)
cls.conn = connection_manager.get_connection(*cls.args)
livetest.cleanup(cls.conn)
livetest.ensure_pool(cls.conn)
cls.inventory = livetest.inventory(cls.conn)
super().setUpClass()
@classmethod
def tearDownClass(cls):
try:
livetest.remove_pool(cls.conn)
finally:
super().tearDownClass()
if livetest.inventory(cls.conn) != cls.inventory:
raise AssertionError("Live tests changed objects outside the test namespace")
def tearDown(self):
livetest.cleanup(self.conn)
def vm(self, name):
disk = livetest.create_volume(self.conn, P + name)
dom = livetest.define_vm(self.conn, P + name, [disk], extra_devices=DEVICES)
return dom, wvmInstance(*self.args, None, uuid=dom.UUIDString())
def persistent(self, dom):
return etree.fromstring(dom.XMLDesc(PERSISTENT))
def test_edits_of_a_stopped_vm_keep_the_vnc_password(self):
for name, (edit, _) in EDITS.items():
with self.subTest(name):
try:
dom, vm = self.vm("xml-stopped")
edit(vm)
self.assertEqual(self.persistent(dom).xpath("string(devices/graphics/@passwd)"), "keep1234")
finally:
livetest.cleanup(self.conn)
def test_edits_of_a_running_vm_keep_pending_changes(self):
for name, (edit, while_running) in EDITS.items():
if not while_running:
continue
with self.subTest(name):
try:
dom, vm = self.vm("xml-running")
dom.create()
# A pending change: only the persistent definition has 192 MiB.
tree = self.persistent(dom)
tree.find("memory").text = str(192 * 1024)
tree.find("currentMemory").text = str(192 * 1024)
self.conn.defineXML(etree.tostring(tree).decode())
edit(vm)
after = self.persistent(dom)
self.assertEqual(after.findtext("memory"), str(192 * 1024), "pending memory change lost")
self.assertEqual(after.xpath("string(devices/graphics/@passwd)"), "keep1234")
finally:
livetest.cleanup(self.conn)