1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-10 04:06:47 +00:00

fix(instances): a running VM's boot order, video and console forms show the pending settings

On an active VM the boot order editor, the video model select and the
console form showed the running VM: a pending boot order, video model or
console setting looked unset, and the boot order editor offered devices
of the running VM only. Choosing one of those cleared the pending order,
since the setter dropped the old order and skipped devices it could not
find. They now read the persistent definition (get_bootmenu,
get_bootorder, get_media_devices, get_video_model and the console
getters take config=), and set_bootorder refuses a device that is not in
it before it changes anything.
This commit is contained in:
catborise 2026-10-11 14:18:20 +03:00
parent 042530fbfd
commit d0bd2ac32b
9 changed files with 152 additions and 35 deletions

View file

@ -732,7 +732,7 @@ class wvmInstance(wvmConnect):
return self._cached(("volume", path), read)
def get_media_devices(self):
def get_media_devices(self, config=False):
def disks(doc):
result = []
for media in doc.xpath("/domain/devices/disk[@device='cdrom']"):
@ -753,10 +753,10 @@ class wvmInstance(wvmConnect):
)
return result
return util.get_xml_path(self._XMLDesc(0), func=disks)
return util.get_xml_path(self._XMLDesc(PERSISTENT_XML if config else 0), func=disks)
def get_bootmenu(self):
menu = util.get_xml_path(self._XMLDesc(0), "/domain/os/bootmenu/@enable")
def get_bootmenu(self, config=False):
menu = util.get_xml_path(self._XMLDesc(PERSISTENT_XML if config else 0), "/domain/os/bootmenu/@enable")
return menu == "yes"
def set_bootmenu(self, flag):
@ -783,10 +783,10 @@ class wvmInstance(wvmConnect):
xmldom = etree.tostring(tree).decode()
self._defineXML(xmldom)
def get_bootorder(self):
def get_bootorder(self, config=False):
boot_order = {}
dev_type = target = None
tree = ElementTree.fromstring(self._XMLDesc(0))
tree = ElementTree.fromstring(self._XMLDesc(PERSISTENT_XML if config else 0))
os = tree.find("os")
boot = os.findall("boot")
@ -858,7 +858,10 @@ class wvmInstance(wvmConnect):
elif dev["type"] == "network": # any NIC type: network, bridge, direct
devices = tree.xpath("./devices/interface[mac/@address=$mac]", mac=dev["dev"])
else:
raise Exception("Invalid Device Type for boot order")
raise util.OperationError("Invalid Device Type for boot order")
# nothing is defined until every choice is found: the order above is cleared
if not devices:
raise util.OperationError(f"Boot device {dev['dev']} is not in the definition")
for d in devices:
d.append(order)
self._defineXML(etree.tostring(tree).decode())
@ -1252,13 +1255,13 @@ class wvmInstance(wvmConnect):
telnet_port = service_port
return telnet_port
def get_console_listener_addr(self):
def get_console_listener_addr(self, config=False):
listener_addr = util.get_xml_path(
self._XMLDesc(0), "/domain/devices/graphics/@listen"
self._XMLDesc(PERSISTENT_XML if config else 0), "/domain/devices/graphics/@listen"
)
if listener_addr is None:
listener_addr = util.get_xml_path(
self._XMLDesc(0), "/domain/devices/graphics/listen/@address"
self._XMLDesc(PERSISTENT_XML if config else 0), "/domain/devices/graphics/listen/@address"
)
if listener_addr is None:
return "127.0.0.1"
@ -1341,9 +1344,9 @@ class wvmInstance(wvmConnect):
)
return websocket_port
def get_console_passwd(self):
def get_console_passwd(self, config=False):
return util.get_xml_path(
self._XMLDesc(VIR_DOMAIN_XML_SECURE), "/domain/devices/graphics/@passwd"
self._XMLDesc(PERSISTENT_XML if config else VIR_DOMAIN_XML_SECURE), "/domain/devices/graphics/@passwd"
)
def set_console_passwd(self, passwd):
@ -1374,17 +1377,17 @@ class wvmInstance(wvmConnect):
newxml = etree.tostring(root).decode()
self._defineXML(newxml)
def get_console_keymap(self):
def get_console_keymap(self, config=False):
return (
util.get_xml_path(
self._XMLDesc(VIR_DOMAIN_XML_SECURE), "/domain/devices/graphics/@keymap"
self._XMLDesc(PERSISTENT_XML if config else VIR_DOMAIN_XML_SECURE), "/domain/devices/graphics/@keymap"
)
or ""
)
def get_video_model(self):
def get_video_model(self, config=False):
""":return only primary video card"""
xml = self._XMLDesc(VIR_DOMAIN_XML_SECURE)
xml = self._XMLDesc(PERSISTENT_XML if config else VIR_DOMAIN_XML_SECURE)
tree = etree.fromstring(xml)
video_models = tree.xpath("/domain/devices/video/model")
for model in video_models:

View file

@ -1,6 +1,7 @@
"""Device edits of a VM: each definition is changed as it describes the
device, every NIC type can be a boot device, posted values stay values, and
definition changes go through _defineXML, a QoS edit keeps the floor, and the CPU, title and description are read from either definition. An active VM's memory is resized
definition changes go through _defineXML, a QoS edit keeps the floor, and what the edit forms show is read from either definition. A boot order with a
missing device changes nothing. An active VM's memory is resized
by balloon, a stopped one's in its definition. Enabling vCPU hot plug keeps
the vCPUs enabled at boot."""
@ -18,15 +19,17 @@ from vrtManager import util
from vrtManager.instance import PERSISTENT_XML, wvmInstance
from libvirt import VIR_DOMAIN_AFFECT_CONFIG, VIR_DOMAIN_AFFECT_LIVE
PERSISTENT = """<domain><title>pending</title><vcpu current='2'>4</vcpu><os><type>hvm</type></os><devices>
PERSISTENT = """<domain><title>pending</title><vcpu current='2'>4</vcpu><os><type>hvm</type><bootmenu enable='yes'/></os><devices>
<disk type='file' device='disk'><source file='/pool/a.qcow2'/><target dev='vda' bus='virtio'/></disk>
<disk type='file' device='disk'><source file='/pool/b.qcow2'/><target dev='vdb' bus='virtio'/></disk>
<interface type='bridge'><mac address='52:54:10:00:00:02'/><source bridge='br0'/></interface>
<interface type='network'><mac address='52:54:10:00:00:01'/><source network='default'/>
<bandwidth><inbound average='1' peak='2' burst='3' floor='4'/></bandwidth></interface>
<disk type='file' device='cdrom'><target dev='sda' bus='sata'/><boot order='1'/></disk>
<video><model type='vga'/></video>
<graphics type='vnc' passwd='pending' keymap='de'><listen type='address' address='0.0.0.0'/></graphics>
</devices></domain>"""
LIVE = """<domain><vcpu>1</vcpu><devices>
LIVE = """<domain><vcpu>1</vcpu><os><type>hvm</type></os><devices>
<disk type='file' device='disk'><source file='/pool/a.qcow2'/><target dev='vda' bus='virtio'/><alias name='virtio-disk0'/></disk>
</devices></domain>"""
@ -120,6 +123,25 @@ class DeviceEditsTestCase(unittest.TestCase):
self.assertEqual((p.get_vcpu(), p.get_cur_vcpu(), p.get_title(), p.get_description()), (1, 1, "", ""))
self.assertEqual((p.get_vcpu(config=True), p.get_cur_vcpu(config=True), p.get_title(config=True)), (4, 2, "pending"))
def test_boot_video_and_console_are_read_from_either_definition(self):
p = proxy()
live = (p.get_bootmenu(), p.get_bootorder(), p.get_media_devices(), p.get_video_model(),
p.get_console_listener_addr(), p.get_console_passwd(), p.get_console_keymap())
self.assertEqual(live, (False, {}, [], None, "127.0.0.1", None, ""))
self.assertEqual((p.get_bootmenu(config=True), p.get_bootorder(config=True), p.get_video_model(config=True)),
(True, {0: {"type": "file", "dev": "cdrom", "target": "sda"}}, "vga"))
self.assertEqual([m["dev"] for m in p.get_media_devices(config=True)], ["sda"])
self.assertEqual((p.get_console_listener_addr(config=True), p.get_console_passwd(config=True),
p.get_console_keymap(config=True)), ("0.0.0.0", "pending", "de"))
def test_a_boot_order_with_a_device_not_in_the_definition_changes_nothing(self):
p = proxy()
for choice in ({"type": "disk", "dev": "vdz"}, {"type": "network", "dev": "52:54:10:00:00:99"},
{"type": "floppy", "dev": "fda"}):
with self.subTest(choice), self.assertRaises(util.OperationError):
p.set_bootorder({0: {"type": "disk", "dev": "vda"}, 1: choice})
p._defineXML.assert_not_called()
def test_the_default_nic_model_leaves_the_choice_to_libvirt(self):
p = proxy()
p.change_network("52:54:10:00:00:01", "52:54:10:00:00:01", "default", "net", "default", "")