Skip to content

Commit 4276bf9

Browse files
fix(drivers): address PR #172 review comments
- _domain2machine: fall back to standard libvirt <vcpu>/<currentMemory>/ <memory> elements when a domain lacks Genesis metadata (e.g. a pre-existing/foreign VM on a shared hypervisor) instead of crashing pool listing for the whole hypervisor. New _memory_mib() handles the unit conversion, since a foreign domain's <memory> isn't guaranteed to be in MiB like Genesis's own tag. - Fix the `docement` -> `document` typo in document_set_tag/ document_meta_set_tag. - _find_attached_volume_element and friends: use `.findall(".//devices/disk")` instead of `.find("devices").findall("disk")`, consistent with the rest of the file, so a domain without a <devices> element doesn't crash with AttributeError. - resize_volume: replace a while-loop-as-one-shot-if with an actual `if`, and guard against a disk with no <target dev=...>. - create_machine: raise a clear error instead of AttributeError when the storage pool's XML has no <target><path>. - rename_machine: `except:` -> `except Exception:` (PEP 8). - Use libvirt.VIR_ERR_NO_SUPPORT instead of the bare int 3. Verified two other flagged spots are not actually reachable and left alone: Element.get(None) returns None rather than raising (checked empirically), and MetaMachine.port_info defaults to `dict` at the model level, so it's never None to begin with.
1 parent 860a7bf commit 4276bf9

2 files changed

Lines changed: 134 additions & 42 deletions

File tree

gcl_sdk/agents/universal/drivers/libvirt.py

Lines changed: 79 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,23 @@
3737
CONSOLE_LOG_DIR = "/var/log/libvirt/qemu"
3838

3939

40+
def _memory_mib(el: ET.Element) -> int:
41+
"""Read a libvirt <memory>/<currentMemory> element's value, in MiB.
42+
43+
libvirt defaults the `unit` attribute to KiB when absent; Genesis's
44+
own genesis:mem tag (handled separately) is always a plain MiB int.
45+
"""
46+
value = int(el.text)
47+
unit = (el.get("unit") or "KiB").lower()
48+
if unit in ("gib", "gb", "g"):
49+
return value * 1024
50+
if unit in ("mib", "mb", "m"):
51+
return value
52+
if unit in ("b", "bytes"):
53+
return value // (1024 * 1024)
54+
return value // 1024 # KiB, libvirt's own default
55+
56+
4057
def _interface_source(
4158
iface: ET.Element, source_el: tp.Optional[ET.Element]
4259
) -> tp.Optional[str]:
@@ -261,45 +278,45 @@ def add_element(
261278
@classmethod
262279
def document_set_tag(
263280
cls,
264-
docement: minidom.Document,
281+
document: minidom.Document,
265282
tag_name: str,
266283
text: tp.Optional[str] = None,
267284
meta_tag: tp.Optional[str] = None,
268285
parent: tp.Optional[minidom.Element] = None,
269286
**kwargs,
270287
) -> None:
271-
root = parent or docement.firstChild
288+
root = parent or document.firstChild
272289
# Firstly we need to remove the old value
273290
for node in root.getElementsByTagName(tag_name):
274291
root.removeChild(node)
275292

276293
# Also we need to remove the old value from the meta
277294
if meta_tag is not None:
278-
meta_node = docement.getElementsByTagName(META_TAG)[0]
279-
for node in docement.getElementsByTagName(meta_tag):
295+
meta_node = document.getElementsByTagName(META_TAG)[0]
296+
for node in document.getElementsByTagName(meta_tag):
280297
meta_node.removeChild(node)
281298

282299
# Add the new value
283-
cls.add_element(docement, meta_tag, parent=meta_node, text=text)
300+
cls.add_element(document, meta_tag, parent=meta_node, text=text)
284301

285302
# Set the new value
286-
cls.add_element(docement, tag_name, parent=root, text=text, **kwargs)
303+
cls.add_element(document, tag_name, parent=root, text=text, **kwargs)
287304

288305
@classmethod
289306
def document_meta_set_tag(
290307
cls,
291-
docement: minidom.Document,
308+
document: minidom.Document,
292309
tag: str,
293310
text: tp.Optional[str] = None,
294311
**kwargs,
295312
) -> None:
296313
# Remove the old value from the meta
297-
meta_node = docement.getElementsByTagName(META_TAG)[0]
298-
for node in docement.getElementsByTagName(tag):
314+
meta_node = document.getElementsByTagName(META_TAG)[0]
315+
for node in document.getElementsByTagName(tag):
299316
meta_node.removeChild(node)
300317

301318
# Add the new value
302-
cls.add_element(docement, tag, parent=meta_node, text=text, **kwargs)
319+
cls.add_element(document, tag, parent=meta_node, text=text, **kwargs)
303320

304321

305322
class XMLLibvirtVolume(XMLLibvirtMixin):
@@ -603,10 +620,24 @@ def _domain2machine(
603620
) -> tp.Tuple[pool_base.Machine, tp.Tuple[pool_base.Port, ...]]:
604621
element = element or ET.fromstring(domain.XMLDesc())
605622

623+
# listAllDomains() enumerates every domain libvirt knows about, not
624+
# just ones this driver created - a domain without Genesis metadata
625+
# (pre-existing/foreign, or from before these tags existed) falls
626+
# back to the standard libvirt elements every domain has, rather
627+
# than crashing pool listing for the whole hypervisor.
606628
cores_xml = element.find(f".//{{{GENESIS_NS}}}vcpu")
629+
if cores_xml is None:
630+
cores_xml = element.find(".//vcpu")
607631
cores = int(cores_xml.text)
632+
608633
ram_xml = element.find(f".//{{{GENESIS_NS}}}mem")
609-
ram = int(ram_xml.text)
634+
if ram_xml is not None:
635+
ram = int(ram_xml.text)
636+
else:
637+
mem_xml = element.find(".//currentMemory")
638+
if mem_xml is None:
639+
mem_xml = element.find(".//memory")
640+
ram = _memory_mib(mem_xml)
610641
image_el = element.find(f".//{{{GENESIS_NS}}}image")
611642
image = image_el.get("uri") if image_el is not None else None
612643

@@ -820,7 +851,7 @@ def _find_attached_volume_element(
820851
self, domain: ET.Element, volume: pool_base.MachineVolume
821852
) -> tp.Optional[ET.Element]:
822853
# Check the volume is attached to the domain
823-
for disk in domain.find("devices").findall("disk"):
854+
for disk in domain.findall(".//devices/disk"):
824855
# Check source and path
825856
source_node = disk.find("source")
826857
if source_node is None:
@@ -1002,7 +1033,7 @@ def delete_volume(self, volume: pool_base.MachineVolume) -> None:
10021033
v.wipe()
10031034
except libvirt.libvirtError as e:
10041035
# Some backends don't need wiping, for ex. ZFS
1005-
if e.get_error_code() != 3: # VIR_ERR_NO_SUPPORT
1036+
if e.get_error_code() != libvirt.VIR_ERR_NO_SUPPORT:
10061037
raise
10071038
max_iters = 20
10081039
for i in range(max_iters + 1):
@@ -1039,7 +1070,7 @@ def attach_volume(self, volume: pool_base.MachineVolume) -> None:
10391070
# FIXME(akremenetsky): Use the simplest check by UUID
10401071
# FIXME(akremenetsky): Check volume name in path. We can check by
10411072
# name as the name is actual UUID of the volume.
1042-
disks = domain_element.find("devices").findall("disk")
1073+
disks = domain_element.findall(".//devices/disk")
10431074
for disk in disks:
10441075
if bytes(volume.name, "utf-8") in ET.tostring(disk):
10451076
raise pool_base.VolumeAlreadyAttachedError(
@@ -1061,7 +1092,7 @@ def attach_volume(self, volume: pool_base.MachineVolume) -> None:
10611092
image_path = vir_volume.path()
10621093

10631094
# Detect next device name from domain XML
1064-
devices = len(domain_element.find("devices").findall("disk"))
1095+
devices = len(domain_element.findall(".//devices/disk"))
10651096
device_name = "vd" + chr(ord("a") + devices)
10661097

10671098
disk_xml = XMLLibvirtInstance.disk_device_xml(image_path, device_name)
@@ -1143,34 +1174,34 @@ def resize_volume(self, volume: pool_base.MachineVolume) -> None:
11431174

11441175
# If the volume is attached and a domain is active use
11451176
# `blockResize`
1146-
while volume.machine is not None:
1177+
if volume.machine is not None:
11471178
domain = self._client.lookupByUUIDString(str(volume.machine))
11481179

1149-
# Not active, break
1150-
if domain.isActive() == 0:
1151-
break
1152-
1153-
domain_xml = domain.XMLDesc()
1154-
domain_element = ET.fromstring(domain_xml)
1155-
disk = self._find_attached_volume_element(domain_element, volume)
1156-
if disk is None:
1157-
raise pool_base.VolumeNotAttachedError(
1158-
volume=volume.uuid, machine=volume.machine
1180+
if domain.isActive() != 0:
1181+
domain_xml = domain.XMLDesc()
1182+
domain_element = ET.fromstring(domain_xml)
1183+
disk = self._find_attached_volume_element(domain_element, volume)
1184+
if disk is None:
1185+
raise pool_base.VolumeNotAttachedError(
1186+
volume=volume.uuid, machine=volume.machine
1187+
)
1188+
target = disk.find("target")
1189+
dev = target.get("dev") if target is not None else None
1190+
if not dev:
1191+
raise ValueError(f"Disk {disk} has no target dev")
1192+
1193+
pool_xml = ET.fromstring(storage_pool.XMLDesc())
1194+
pool_type = StoragePoolType(pool_xml.get("type"))
1195+
1196+
# ZFS zvols are block devices; QEMU cannot grow them directly.
1197+
# Resize the zvol first, then notify the running guest.
1198+
if pool_type == StoragePoolType.ZFS:
1199+
vir_volume.resize(new_size_bytes)
1200+
1201+
domain.blockResize(
1202+
dev, new_size_bytes, libvirt.VIR_DOMAIN_BLOCK_RESIZE_BYTES
11591203
)
1160-
dev = disk.find("target").get("dev")
1161-
1162-
pool_xml = ET.fromstring(storage_pool.XMLDesc())
1163-
pool_type = StoragePoolType(pool_xml.get("type"))
1164-
1165-
# ZFS zvols are block devices; QEMU cannot grow them directly.
1166-
# Resize the zvol first, then notify the running guest.
1167-
if pool_type == StoragePoolType.ZFS:
1168-
vir_volume.resize(new_size_bytes)
1169-
1170-
domain.blockResize(
1171-
dev, new_size_bytes, libvirt.VIR_DOMAIN_BLOCK_RESIZE_BYTES
1172-
)
1173-
return
1204+
return
11741205

11751206
# Ordinary resize via qemu-img
11761207
vir_volume.resize(new_size_bytes)
@@ -1306,7 +1337,13 @@ def create_machine(
13061337

13071338
storage_pool_xml = ET.fromstring(storage_pool.XMLDesc())
13081339
pool_type = StoragePoolType(storage_pool_xml.get("type"))
1309-
pool_path = storage_pool_xml.find("target").find("path").text
1340+
target_el = storage_pool_xml.find("target")
1341+
path_el = target_el.find("path") if target_el is not None else None
1342+
if path_el is None or not path_el.text:
1343+
raise ValueError(
1344+
f"Storage pool {self._spec.storage_pool} has no target path"
1345+
)
1346+
pool_path = path_el.text
13101347

13111348
# Add the volumes to the domain
13121349
for i, volume in enumerate(volumes):
@@ -1469,7 +1506,7 @@ def rename_machine(self, machine: pool_base.Machine, name: str) -> None:
14691506
ports=ports,
14701507
legacy_machine=legacy_domain,
14711508
)
1472-
except:
1509+
except Exception:
14731510
machine.name = origin_name
14741511
raise
14751512
LOG.debug("The domain %s was renamed to %s", origin_name, name)

gcl_sdk/tests/unit/agents/drivers/test_libvirt.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,64 @@
2424
pytest.importorskip("libvirt")
2525

2626
from gcl_sdk.agents.universal.drivers.libvirt import XMLLibvirtInstance # noqa: E402
27+
from gcl_sdk.agents.universal.drivers.libvirt import _interface_source # noqa: E402
28+
from gcl_sdk.agents.universal.drivers.libvirt import _memory_mib # noqa: E402
2729
from gcl_sdk.agents.universal.drivers.libvirt import domain_template # noqa: E402
2830

2931

32+
class TestMemoryMib:
33+
def test_defaults_to_kib_when_unit_is_absent(self):
34+
el = ET.fromstring("<memory>2097152</memory>")
35+
assert _memory_mib(el) == 2048
36+
37+
def test_kib(self):
38+
el = ET.fromstring('<memory unit="KiB">2097152</memory>')
39+
assert _memory_mib(el) == 2048
40+
41+
def test_mib(self):
42+
el = ET.fromstring('<memory unit="MiB">2048</memory>')
43+
assert _memory_mib(el) == 2048
44+
45+
def test_gib(self):
46+
el = ET.fromstring('<currentMemory unit="GiB">2</currentMemory>')
47+
assert _memory_mib(el) == 2048
48+
49+
def test_bytes(self):
50+
el = ET.fromstring('<memory unit="bytes">2147483648</memory>')
51+
assert _memory_mib(el) == 2048
52+
53+
54+
class TestInterfaceSource:
55+
def test_no_source_element(self):
56+
iface = ET.fromstring("<interface type='network'/>")
57+
assert _interface_source(iface, None) is None
58+
59+
def test_prefers_network_attribute_over_bridge(self):
60+
# libvirt rewrites type='network' interfaces backed by a
61+
# forward=bridge network to type='bridge' in the live domain XML,
62+
# but keeps both attributes on <source> - the logical name is
63+
# what the orchestrator's target state actually tracks.
64+
iface = ET.fromstring(
65+
"<interface type='bridge'>"
66+
"<source network='exordos-core-net' bridge='br-core'/>"
67+
"</interface>"
68+
)
69+
source_el = iface.find("source")
70+
assert _interface_source(iface, source_el) == "exordos-core-net"
71+
72+
def test_falls_back_to_own_type_attribute(self):
73+
iface = ET.fromstring(
74+
"<interface type='bridge'><source bridge='br-core'/></interface>"
75+
)
76+
source_el = iface.find("source")
77+
assert _interface_source(iface, source_el) == "br-core"
78+
79+
def test_missing_type_attribute_does_not_crash(self):
80+
iface = ET.fromstring("<interface><source bridge='br-core'/></interface>")
81+
source_el = iface.find("source")
82+
assert _interface_source(iface, source_el) is None
83+
84+
3085
def test_domain_console_logs_to_file():
3186
log_path = "/var/log/libvirt/qemu/test-vm.console.log"
3287

0 commit comments

Comments
 (0)