Skip to content

Commit e1bb7ee

Browse files
committed
Improve VirtualBox descriptor parsing
1 parent b19a789 commit e1bb7ee

15 files changed

Lines changed: 647 additions & 446 deletions

File tree

Lines changed: 101 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,115 @@
11
from __future__ import annotations
22

3+
from datetime import datetime
34
from typing import TYPE_CHECKING, TextIO
5+
from uuid import UUID
46

57
from defusedxml import ElementTree
68

79
if TYPE_CHECKING:
8-
from collections.abc import Iterator
910
from xml.etree.ElementTree import Element
1011

12+
NS = "{http://www.virtualbox.org/}"
1113

12-
class VBox:
13-
VBOX_XML_NAMESPACE = "{http://www.virtualbox.org/}"
1414

15+
class VBox:
1516
def __init__(self, fh: TextIO):
1617
self._xml: Element = ElementTree.fromstring(fh.read())
18+
if self._xml.tag != f"{NS}VirtualBox":
19+
raise ValueError("Invalid VirtualBox XML descriptor: root element is not VirtualBox")
20+
21+
self.machine = self._xml.find(f"./{NS}Machine")
22+
if self.machine is None:
23+
raise ValueError("Invalid VirtualBox XML descriptor: no Machine element found")
24+
25+
self.media: dict[str, HardDisk] = {}
26+
27+
stack = [(None, element) for element in self.machine.find(f"./{NS}MediaRegistry/{NS}HardDisks")]
28+
while stack:
29+
parent, element = stack.pop()
30+
hdd = HardDisk(self, element, parent)
31+
32+
self.media[hdd.uuid] = hdd
33+
stack.extend([(hdd, child) for child in list(element)])
34+
35+
if (element := self.machine.find(f"./{NS}Hardware")) is None:
36+
raise ValueError("Invalid VirtualBox XML descriptor: no Hardware element found")
37+
self.hardware = Hardware(self, element)
38+
39+
@property
40+
def uuid(self) -> UUID | None:
41+
"""The VM UUID."""
42+
if self.machine is not None:
43+
return UUID(self.machine.get("uuid").strip("{}"))
44+
return None
45+
46+
@property
47+
def name(self) -> str | None:
48+
"""The VM name."""
49+
if self.machine is not None:
50+
return self.machine.get("name")
51+
return None
52+
53+
@property
54+
def snapshots(self) -> list[Snapshot]:
55+
"""All snapshots."""
56+
# Snapshots have a weird structure, since we don't do much with it for now, just get them flatly
57+
# TODO: Implement proper snapshot tree structure
58+
return [Snapshot(self, element) for element in self.machine.findall(f".//{NS}Snapshot")]
59+
60+
61+
class HardDisk:
62+
def __init__(self, vbox: VBox, element: Element, parent: HardDisk | None = None):
63+
self.vbox = vbox
64+
self.element = element
65+
self.parent = parent
66+
67+
@property
68+
def uuid(self) -> UUID:
69+
"""The disk UUID."""
70+
return UUID(self.element.get("uuid").strip("{}"))
71+
72+
@property
73+
def location(self) -> str:
74+
"""The disk location."""
75+
return self.element.get("location")
76+
77+
78+
class Snapshot:
79+
def __init__(self, vbox: VBox, element: Element):
80+
self.vbox = vbox
81+
self.element = element
82+
83+
@property
84+
def uuid(self) -> UUID:
85+
"""The snapshot UUID."""
86+
return UUID(self.element.get("uuid").strip("{}"))
87+
88+
@property
89+
def name(self) -> str:
90+
"""The snapshot name."""
91+
return self.element.get("name")
92+
93+
@property
94+
def ts(self) -> datetime:
95+
"""The snapshot timestamp."""
96+
return datetime.strptime(self.element.get("timeStamp"), "%Y-%m-%dT%H:%M:%S%z")
97+
98+
@property
99+
def hardware(self) -> Hardware:
100+
"""The snapshot hardware state."""
101+
return Hardware(self.vbox, self.element.find(f"./{NS}Hardware"))
102+
103+
104+
class Hardware:
105+
def __init__(self, vbox: VBox, element: Element):
106+
self.vbox = vbox
107+
self.element = element
17108

18-
def disks(self) -> Iterator[str]:
19-
for hdd_elem in self._xml.findall(f".//{self.VBOX_XML_NAMESPACE}HardDisk[@location][@type='Normal']"):
20-
# Allow format specifier to be case-insensitive (i.e. VDI, vdi)
21-
if (format := hdd_elem.get("format")) and format.lower() == "vdi":
22-
yield hdd_elem.attrib["location"]
109+
@property
110+
def disks(self) -> list[HardDisk]:
111+
"""All attached hard disks."""
112+
images = self.element.findall(
113+
f"./{NS}StorageControllers/{NS}StorageController/{NS}AttachedDevice[@type='HardDisk']/{NS}Image"
114+
)
115+
return [self.vbox.media[UUID(image.get("uuid").strip("{}"))] for image in images]
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
<?xml version="1.0"?>
2+
<!--
3+
** DO NOT EDIT THIS FILE.
4+
** If you make changes to this file while any VirtualBox related application
5+
** is running, your changes will be overwritten later, without taking effect.
6+
** Use VBoxManage or the VirtualBox Manager GUI to make changes.
7+
**
8+
** Written by VirtualBox 7.2.4_RPMFUSION (r170995)
9+
-->
10+
<VirtualBox xmlns="http://www.virtualbox.org/" version="1.19-linux">
11+
<Machine uuid="{a6277950-3d1b-45d3-b2fd-dc1f385027e1}" name="GOAD-DC01" OSType="Windows2016_64" currentSnapshot="{264ccd7e-9ffd-45ba-bd0e-4e1968d3355a}" snapshotFolder="Snapshots" lastStateChange="2025-12-19T21:00:43Z">
12+
<MediaRegistry>
13+
<HardDisks>
14+
<HardDisk uuid="{0898f36f-01c3-4b43-8aeb-36ba7adaef95}" location="WindowsServer2019-disk001.vmdk" format="VMDK" type="Normal">
15+
<HardDisk uuid="{3c72ec80-dc73-4448-a63e-97970cdd87e5}" location="Snapshots/{3c72ec80-dc73-4448-a63e-97970cdd87e5}.vmdk" format="VMDK">
16+
<HardDisk uuid="{e6800503-8273-4f16-b584-c7ba2c1df698}" location="Snapshots/{e6800503-8273-4f16-b584-c7ba2c1df698}.vmdk" format="VMDK"/>
17+
</HardDisk>
18+
</HardDisk>
19+
</HardDisks>
20+
</MediaRegistry>
21+
<Snapshot uuid="{95b1572d-c893-48f0-9bc9-6a01a0fd2cb6}" name="push_1766163705_7292" timeStamp="2025-12-19T17:01:46Z">
22+
<Hardware>
23+
<Memory RAMSize="3000"/>
24+
<Boot>
25+
<Order position="1" device="HardDisk"/>
26+
<Order position="2" device="DVD"/>
27+
<Order position="3" device="None"/>
28+
<Order position="4" device="None"/>
29+
</Boot>
30+
<Display VRAMSize="128"/>
31+
<RemoteDisplay enabled="true">
32+
<VRDEProperties>
33+
<Property name="TCP/Address" value="127.0.0.1"/>
34+
<Property name="TCP/Ports" value="5902"/>
35+
</VRDEProperties>
36+
</RemoteDisplay>
37+
<Firmware/>
38+
<BIOS>
39+
<IOAPIC enabled="true"/>
40+
<SmbiosUuidLittleEndian enabled="true"/>
41+
</BIOS>
42+
<Network>
43+
<Adapter slot="0" enabled="true" MACAddress="0800277AA2FC" type="82540EM">
44+
<NAT localhost-reachable="true">
45+
<DNS use-proxy="true"/>
46+
<Forwarding name="ssh" proto="1" hostip="127.0.0.1" hostport="2222" guestport="22"/>
47+
<Forwarding name="winrm" proto="1" hostip="127.0.0.1" hostport="55985" guestport="5985"/>
48+
<Forwarding name="winrm-ssl" proto="1" hostip="127.0.0.1" hostport="55986" guestport="5986"/>
49+
</NAT>
50+
</Adapter>
51+
<Adapter slot="1" enabled="true" MACAddress="0800278CC821" type="82540EM">
52+
<DisabledModes>
53+
<NAT localhost-reachable="true"/>
54+
<InternalNetwork name="intnet"/>
55+
<NATNetwork name="NatNetwork"/>
56+
</DisabledModes>
57+
<HostOnlyInterface name="vboxnet0"/>
58+
</Adapter>
59+
</Network>
60+
<AudioAdapter driver="ALSA"/>
61+
<Clipboard mode="Bidirectional"/>
62+
<StorageControllers>
63+
<StorageController name="IDE Controller" type="PIIX4" PortCount="2" useHostIOCache="true" Bootable="true">
64+
<AttachedDevice type="HardDisk" hotpluggable="false" port="0" device="0">
65+
<Image uuid="{0898f36f-01c3-4b43-8aeb-36ba7adaef95}"/>
66+
</AttachedDevice>
67+
</StorageController>
68+
</StorageControllers>
69+
<RTC localOrUTC="UTC"/>
70+
<CPU count="2">
71+
<HardwareVirtExLargePages enabled="false"/>
72+
<PAE enabled="true"/>
73+
<LongMode enabled="true"/>
74+
</CPU>
75+
</Hardware>
76+
<Snapshots>
77+
<Snapshot uuid="{264ccd7e-9ffd-45ba-bd0e-4e1968d3355a}" name="push_1766170151_8843" timeStamp="2025-12-19T18:49:11Z">
78+
<Hardware>
79+
<Memory RAMSize="3000"/>
80+
<Boot>
81+
<Order position="1" device="HardDisk"/>
82+
<Order position="2" device="DVD"/>
83+
<Order position="3" device="None"/>
84+
<Order position="4" device="None"/>
85+
</Boot>
86+
<Display VRAMSize="128"/>
87+
<RemoteDisplay enabled="true">
88+
<VRDEProperties>
89+
<Property name="TCP/Address" value="127.0.0.1"/>
90+
<Property name="TCP/Ports" value="5902"/>
91+
</VRDEProperties>
92+
</RemoteDisplay>
93+
<Firmware/>
94+
<BIOS>
95+
<IOAPIC enabled="true"/>
96+
<SmbiosUuidLittleEndian enabled="true"/>
97+
</BIOS>
98+
<Network>
99+
<Adapter slot="0" enabled="true" MACAddress="0800277AA2FC" type="82540EM">
100+
<NAT localhost-reachable="true">
101+
<DNS use-proxy="true"/>
102+
<Forwarding name="ssh" proto="1" hostip="127.0.0.1" hostport="2222" guestport="22"/>
103+
<Forwarding name="winrm" proto="1" hostip="127.0.0.1" hostport="55985" guestport="5985"/>
104+
<Forwarding name="winrm-ssl" proto="1" hostip="127.0.0.1" hostport="55986" guestport="5986"/>
105+
</NAT>
106+
</Adapter>
107+
<Adapter slot="1" enabled="true" MACAddress="0800278CC821" type="82540EM">
108+
<DisabledModes>
109+
<NAT localhost-reachable="true"/>
110+
<InternalNetwork name="intnet"/>
111+
<NATNetwork name="NatNetwork"/>
112+
</DisabledModes>
113+
<HostOnlyInterface name="vboxnet0"/>
114+
</Adapter>
115+
</Network>
116+
<AudioAdapter driver="ALSA"/>
117+
<Clipboard mode="Bidirectional"/>
118+
<StorageControllers>
119+
<StorageController name="IDE Controller" type="PIIX4" PortCount="2" useHostIOCache="true" Bootable="true">
120+
<AttachedDevice type="HardDisk" hotpluggable="false" port="0" device="0">
121+
<Image uuid="{3c72ec80-dc73-4448-a63e-97970cdd87e5}"/>
122+
</AttachedDevice>
123+
</StorageController>
124+
</StorageControllers>
125+
<RTC localOrUTC="UTC"/>
126+
<CPU count="2">
127+
<HardwareVirtExLargePages enabled="false"/>
128+
<PAE enabled="true"/>
129+
<LongMode enabled="true"/>
130+
</CPU>
131+
</Hardware>
132+
</Snapshot>
133+
</Snapshots>
134+
</Snapshot>
135+
<Hardware>
136+
<Memory RAMSize="3000"/>
137+
<Boot>
138+
<Order position="1" device="HardDisk"/>
139+
<Order position="2" device="DVD"/>
140+
<Order position="3" device="None"/>
141+
<Order position="4" device="None"/>
142+
</Boot>
143+
<Display VRAMSize="128"/>
144+
<RemoteDisplay enabled="true">
145+
<VRDEProperties>
146+
<Property name="TCP/Address" value="127.0.0.1"/>
147+
<Property name="TCP/Ports" value="5902"/>
148+
</VRDEProperties>
149+
</RemoteDisplay>
150+
<Firmware/>
151+
<BIOS>
152+
<IOAPIC enabled="true"/>
153+
<SmbiosUuidLittleEndian enabled="true"/>
154+
</BIOS>
155+
<Network>
156+
<Adapter slot="0" enabled="true" MACAddress="0800277AA2FC" type="82540EM">
157+
<NAT localhost-reachable="true">
158+
<DNS use-proxy="true"/>
159+
<Forwarding name="ssh" proto="1" hostip="127.0.0.1" hostport="2222" guestport="22"/>
160+
<Forwarding name="winrm" proto="1" hostip="127.0.0.1" hostport="55985" guestport="5985"/>
161+
<Forwarding name="winrm-ssl" proto="1" hostip="127.0.0.1" hostport="55986" guestport="5986"/>
162+
</NAT>
163+
</Adapter>
164+
<Adapter slot="1" enabled="true" MACAddress="0800278CC821" type="82540EM">
165+
<DisabledModes>
166+
<NAT localhost-reachable="true"/>
167+
<InternalNetwork name="intnet"/>
168+
<NATNetwork name="NatNetwork"/>
169+
</DisabledModes>
170+
<HostOnlyInterface name="vboxnet0"/>
171+
</Adapter>
172+
</Network>
173+
<AudioAdapter driver="ALSA"/>
174+
<Clipboard mode="Bidirectional"/>
175+
<StorageControllers>
176+
<StorageController name="IDE Controller" type="PIIX4" PortCount="2" useHostIOCache="true" Bootable="true">
177+
<AttachedDevice type="HardDisk" hotpluggable="false" port="0" device="0">
178+
<Image uuid="{e6800503-8273-4f16-b584-c7ba2c1df698}"/>
179+
</AttachedDevice>
180+
</StorageController>
181+
</StorageControllers>
182+
<RTC localOrUTC="UTC"/>
183+
<CPU count="2">
184+
<HardwareVirtExLargePages enabled="false"/>
185+
<PAE enabled="true"/>
186+
<LongMode enabled="true"/>
187+
</CPU>
188+
</Hardware>
189+
<Groups>
190+
<Group name="/GOAD"/>
191+
</Groups>
192+
</Machine>
193+
</VirtualBox>

tests/_util.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
5+
6+
def absolute_path(filename: str) -> Path:
7+
return Path(__file__).parent.joinpath(filename).resolve()

0 commit comments

Comments
 (0)