|
26 | 26 | # collection when they're not available. |
27 | 27 | pytest.importorskip("libvirt") |
28 | 28 |
|
| 29 | +from exordos_core.compute import constants as nc # noqa: E402 |
29 | 30 | from exordos_core.compute.dm import models # noqa: E402 |
30 | 31 | from exordos_core.compute.pool.drivers.libvirt import LibvirtPoolDriver # noqa: E402 |
31 | 32 | from exordos_core.compute.pool.drivers.libvirt import XMLLibvirtInstance # noqa: E402 |
32 | 33 | from exordos_core.compute.pool.drivers.libvirt import domain_template # noqa: E402 |
33 | 34 |
|
34 | 35 |
|
35 | | -def _local_driver() -> LibvirtPoolDriver: |
| 36 | +def _local_driver(network_type: str = "network") -> LibvirtPoolDriver: |
36 | 37 | # libvirt's built-in "test" driver simulates a hypervisor in-memory - |
37 | 38 | # no real virtualization or daemon needed, so real libvirt calls |
38 | | - # (lookupByUUIDString, etc.) can be exercised end-to-end. |
39 | | - spec = models.LibvirtPoolDriverSpec(connection_uri="test:///default") |
40 | | - pool = models.MachinePool(uuid=sys_uuid.uuid4(), name="test-pool", driver_spec=spec) |
| 39 | + # (lookupByUUIDString, etc.) can be exercised end-to-end. It even ships |
| 40 | + # a default storage pool ("default-pool"), so create_machine() can be |
| 41 | + # exercised end-to-end too. |
| 42 | + spec = models.LibvirtPoolDriverSpec( |
| 43 | + connection_uri="test:///default", |
| 44 | + network_type=network_type, |
| 45 | + storage_pool="default-pool", |
| 46 | + ) |
| 47 | + pool = models.MachinePool( |
| 48 | + uuid=sys_uuid.uuid4(), name="test-pool", driver_spec=spec |
| 49 | + ) |
41 | 50 | return LibvirtPoolDriver(pool) |
42 | 51 |
|
43 | 52 |
|
| 53 | +def _machine() -> models.Machine: |
| 54 | + return models.Machine( |
| 55 | + uuid=sys_uuid.uuid4(), |
| 56 | + project_id=sys_uuid.uuid4(), |
| 57 | + name="test-vm", |
| 58 | + cores=1, |
| 59 | + ram=512, |
| 60 | + ) |
| 61 | + |
| 62 | + |
| 63 | +def _port(uuid: sys_uuid.UUID, source: str) -> models.Port: |
| 64 | + return models.Port( |
| 65 | + uuid=uuid, |
| 66 | + project_id=sys_uuid.uuid4(), |
| 67 | + mac=models.Port.generate_mac(), |
| 68 | + source=source, |
| 69 | + status=nc.PortStatus.ACTIVE.value, |
| 70 | + ) |
| 71 | + |
| 72 | + |
| 73 | +def _live_interface(driver: LibvirtPoolDriver, machine: models.Machine) -> ET.Element: |
| 74 | + domain = driver._client.lookupByUUIDString(str(machine.uuid)) |
| 75 | + return ET.fromstring(domain.XMLDesc()).find(".//devices/interface") |
| 76 | + |
| 77 | + |
44 | 78 | def test_domain_console_logs_to_file(): |
45 | 79 | log_path = "/var/log/libvirt/qemu/test-vm.console.log" |
46 | 80 |
|
@@ -133,3 +167,61 @@ def test_volume_cleanup_still_runs_when_the_domain_is_already_gone(self): |
133 | 167 | driver.delete_machine(machine, delete_volumes=True) |
134 | 168 |
|
135 | 169 | mock_list_volumes.assert_called_once_with(machine) |
| 170 | + |
| 171 | + |
| 172 | +class TestCreateMachine: |
| 173 | + def test_boot_port_always_uses_network_type_on_a_bridge_hypervisor(self): |
| 174 | + # Regression: a bridge-type hypervisor must not have the boot |
| 175 | + # network's (logical, potentially long) name treated as a literal |
| 176 | + # host bridge device name - libvirt rejects that outright as too |
| 177 | + # long for IFNAMSIZ. |
| 178 | + driver = _local_driver(network_type="bridge") |
| 179 | + machine = _machine() |
| 180 | + port = _port(nc.BOOT_NETWORK_PORT_UUID, source="exordos-core-boot-net") |
| 181 | + |
| 182 | + driver.create_machine(machine, volumes=[], ports=[port]) |
| 183 | + |
| 184 | + interface = _live_interface(driver, machine) |
| 185 | + assert interface.get("type") == "network" |
| 186 | + assert interface.find("source").get("network") == "exordos-core-boot-net" |
| 187 | + |
| 188 | + def test_real_port_honors_the_hypervisors_network_type(self): |
| 189 | + # Regression: on a real bridge-type hypervisor, ports are raw |
| 190 | + # bridge-type interfaces (source=<bridge device>) - not a libvirt |
| 191 | + # network wrapping one. create_machine() is also used by |
| 192 | + # recreate_machine() to rebuild the domain with the real port(s) |
| 193 | + # post-flash, so it must not force type='network' on those. |
| 194 | + driver = _local_driver(network_type="bridge") |
| 195 | + machine = _machine() |
| 196 | + port = _port(sys_uuid.uuid4(), source="br0") |
| 197 | + |
| 198 | + driver.create_machine(machine, volumes=[], ports=[port]) |
| 199 | + |
| 200 | + interface = _live_interface(driver, machine) |
| 201 | + assert interface.get("type") == "bridge" |
| 202 | + assert interface.find("source").get("bridge") == "br0" |
| 203 | + |
| 204 | + |
| 205 | +class TestAttachPort: |
| 206 | + def test_honors_the_hypervisors_network_type_for_bridge_hypervisors(self): |
| 207 | + # Regression: attach_port() is only ever used for the real port, |
| 208 | + # which on a bridge-type hypervisor is a raw bridge-type interface |
| 209 | + # (source=<bridge device>) - not a libvirt network wrapping one. |
| 210 | + # |
| 211 | + # libvirt's test:// driver doesn't support attachDeviceFlags()'s |
| 212 | + # LIVE+CONFIG flag combination, so mock the domain lookup instead |
| 213 | + # of exercising a real domain end-to-end. |
| 214 | + driver = _local_driver(network_type="bridge") |
| 215 | + machine = _machine() |
| 216 | + port = _port(sys_uuid.uuid4(), source="br0") |
| 217 | + |
| 218 | + mock_domain = mock.MagicMock() |
| 219 | + with mock.patch.object( |
| 220 | + driver._client, "lookupByUUIDString", return_value=mock_domain |
| 221 | + ): |
| 222 | + driver.attach_port(machine, port) |
| 223 | + |
| 224 | + interface_xml, _flags = mock_domain.attachDeviceFlags.call_args[0] |
| 225 | + interface = ET.fromstring(interface_xml) |
| 226 | + assert interface.get("type") == "bridge" |
| 227 | + assert interface.find("source").get("bridge") == "br0" |
0 commit comments