"""Which PCI devices of a host a VM can be given: an endpoint in an IOMMU group, without the host's own network interfaces or disks on it, that no other VM names; the other endpoints of its group are managed devices of the same definition, or have no driver or vfio-pci, and no other VM has any of them; bridges stay with pcieport. Each definition of a snapshot is checked on its own. Node XML as libvirt 11.10 writes it on a q35 host with an emulated IOMMU.""" from unittest.mock import MagicMock from django.test import SimpleTestCase from lxml import etree from vrtManager import util from vrtManager.nodedevs import _pci, check_hostdevs, hostdev_problems, pci_problems from vrtManager.tests.fakes import IOMMU_NODES from vrtManager.util import OperationError def inventory(vms=()): """The host's PCI devices, as a list; vms: (VM, its PCI addresses).""" children = {} for name, node in IOMMU_NODES.items(): children.setdefault(node.findtext("parent"), []).append(name) users = [(vm, set(addresses), []) for vm, addresses in vms] return [_pci(name, node, IOMMU_NODES, children, users) for name, node in IOMMU_NODES.items() if node.find("capability").get("type") == "pci"] def problems(address, managed=(), vms=()): devices = {d["address"]: d for d in inventory(vms)} return " ".join(util.render(m["text"], m["values"], str) for m in pci_problems("web-1", devices[address], devices, {address, *managed})) def hostdev(slot, managed="yes"): return (f"" f"
") class PciRulesTests(SimpleTestCase): def test_a_lone_endpoint_bound_to_vfio_pci_can_be_given(self): self.assertEqual(problems("0000:00:07.0"), "") def test_a_device_the_host_uses_can_not_and_how_to_free_it(self): reason = problems("0000:00:06.0") self.assertIn("holds the host's eth1", reason) self.assertIn("bind 0000:00:06.0 to vfio-pci on the host first", reason) def test_a_bridge_of_any_kind_can_not(self): # a PCIe port, and of the bridge class an LPC controller (a host bridge alike) self.assertEqual(problems("0000:00:02.0"), "PCI device 0000:00:02.0 is a bridge") self.assertEqual(problems("0000:00:05.0"), "PCI device 0000:00:05.0 is a bridge") def test_the_other_endpoints_of_its_group_must_be_free_of_the_host(self): # the LPC controller is of class 0x06 but no bridge: its driver keeps the group reason = problems("0000:00:04.0") self.assertIn("with 0000:00:03.0 (SATA controller), which the host (driver ahci) uses", reason) self.assertIn("with 0000:00:05.0 (LPC controller), which the host (driver lpc_ich) uses", reason) def test_unbound_and_vfio_pci_members_and_a_pcieport_bridge_keep_nothing(self): self.assertEqual(problems("0000:00:08.0"), "") def test_a_member_given_as_a_managed_device_too_is_taken_with_it(self): self.assertIn("with 0000:00:0e.0 (GPU audio), which the host (driver snd_hda_intel) uses", problems("0000:00:0d.0")) self.assertEqual(problems("0000:00:0d.0", managed={"0000:00:0e.0"}), "") def test_a_member_another_vm_has_or_that_cannot_be_read_keeps_the_group(self): self.assertIn("with 0000:00:09.0 (NIC port 1), which db-1 uses", problems("0000:00:08.0", vms=[("db-1", {"0000:00:09.0"})])) # managed in this definition or not: another VM has it self.assertIn("which db-1 uses", problems("0000:00:0d.0", managed={"0000:00:0e.0"}, vms=[("db-1", {"0000:00:0e.0"})])) self.assertIn("with 0000:00:0c.0, which cannot be read", problems("0000:00:0b.0")) def test_a_device_another_vm_has_can_not(self): self.assertIn("is given to db-1", problems("0000:00:07.0", vms=[("db-1", {"0000:00:07.0"})])) self.assertEqual(problems("0000:00:07.0", vms=[("web-1", {"0000:00:07.0"})]), "") # its own def test_a_device_the_host_does_not_have_can_not(self): reasons = [util.render(m["text"], m["values"], str) for m in hostdev_problems("web-1", inventory(), [], {"0000:00:1f.0"}, [])] self.assertEqual(reasons, ["PCI device 0000:00:1f.0 is not on the host"]) class DefinitionsTests(SimpleTestCase): """A revert restores two definitions; the VM starts from one of them.""" def check(self, *devices_per_definition): conn = MagicMock() conn.wvm.listAllDevices.return_value = [MagicMock(**{"name.return_value": name, "XMLDesc.return_value": etree.tostring(node)}) for name, node in IOMMU_NODES.items()] conn.domain_trees.return_value = [] trees = [etree.fromstring(f"{''.join(devices)}") for devices in devices_per_definition] check_hostdevs(conn, "web-1", trees) def test_each_definition_takes_its_group_on_its_own(self): self.check([hostdev(0x0d), hostdev(0x0e)]) # the running definition has the GPU, only the saved one its audio function with self.assertRaisesRegex(OperationError, "GPU audio"): self.check([hostdev(0x0d)], [hostdev(0x0e)]) def test_a_member_libvirt_does_not_manage_keeps_its_host_driver(self): with self.assertRaisesRegex(OperationError, "which the host \\(driver snd_hda_intel\\) uses"): self.check([hostdev(0x0d), hostdev(0x0e, managed="no")])