mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 19:56:48 +00:00
test: each test is in the tests package of the code it tests
Tests that build vrtManager objects and touch no view, form or model of an app lived in that app; vrtManager/tests already holds such tests, so the convention was split. They move to vrtManager/tests: the firmware, CPU mode and PCIe port definitions of a new VM (test_create), USB and PCI host devices, the console listener, host CPU reads, interface reads, deletes and XML, network filters, PCI passthrough rules, node devices, network changes, addresses and leases, pool XML, secrets, and the live tests that use vrtManager only (disk resize, ISO, snapshots, XML, live clone). livetest.py, the live-test helper six apps import, and the network test driver move with them. Others go to their app: the quota with a host down to instances, the 401/403 Logs rows (webvirtcloud's middleware) to webvirtcloud, the VM page's tab script test to instances, novncd's own tests out of the console views tests, the compute lock and input validation tests out of the computes views tests, the interface views out of the forms tests. instances/tests/test_views.py, entirely live, is test_live_views.py. Test modules imported fixtures from each other; shared ones are now in vrtManager/tests/fakes.py (migration fakes, node devices, IOMMU groups, leases, a NAT network, a Ceph secret), interfaces/tests/fakes.py, console/tests/daemons.py (novncd loaded once), livetest.wait_for and instances/tests/create_page.BODY. Every live test checks livetest.enabled(). The suite runs the same 2097 tests.
This commit is contained in:
parent
4e0952aa50
commit
bdc8a6755c
73 changed files with 2231 additions and 2132 deletions
|
|
@ -4,169 +4,17 @@ and the VMs that name it, by PCI address, by USB address or by USB model
|
|||
only. Node device XML as libvirt 11.10 writes it; the connection is a mock,
|
||||
libvirt's test driver has no node devices."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
import libvirt
|
||||
from computes.models import Compute
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import SimpleTestCase, TestCase
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from instances.models import Instance
|
||||
from lxml import html
|
||||
from vrtManager.connection import wvmConnect
|
||||
from vrtManager import util
|
||||
from vrtManager.nodedevs import hostdev_problems, node_devices, pci_address
|
||||
|
||||
|
||||
def pci(slot, product, code, driver=None, group=None):
|
||||
iommu = f"<iommuGroup number='{group}'><address domain='0x0000' bus='0x00' slot='{slot}' function='0x0'/></iommuGroup>" \
|
||||
if group is not None else ""
|
||||
driver = f"<driver><name>{driver}</name></driver>" if driver else ""
|
||||
return (f"pci_0000_00_{int(slot, 16):02x}_0", f"""<device><name>pci_0000_00_{int(slot, 16):02x}_0</name><parent>computer</parent>{driver}
|
||||
<capability type='pci'><class>{code}</class><domain>0</domain><bus>0</bus><slot>{int(slot, 16)}</slot><function>0</function>
|
||||
<product id='0x1000'>{product}</product><vendor id='0x1af4'>Red Hat, Inc.</vendor>{iommu}</capability></device>""")
|
||||
|
||||
|
||||
def child(name, parent, capability):
|
||||
return name, f"<device><name>{name}</name><parent>{parent}</parent>{capability}</device>"
|
||||
|
||||
|
||||
def usb(name, parent, bus, device, product="QEMU Tablet", vendor_id="0x0627", product_id="0x0001"):
|
||||
return name, f"""<device><name>{name}</name><parent>{parent}</parent><capability type='usb_device'><bus>{bus}</bus>
|
||||
<device>{device}</device><product id='{product_id}'>{product}</product><vendor id='{vendor_id}'>Adomax</vendor>
|
||||
</capability></device>"""
|
||||
|
||||
|
||||
def interface(name, parent, code):
|
||||
return child(name, parent, f"<capability type='usb'><number>0</number><class>{code}</class></capability>")
|
||||
|
||||
|
||||
NODES = [
|
||||
pci("0x06", "Virtio network device", "0x020000", "virtio-pci", group=3),
|
||||
child("net_eth0", "pci_0000_00_06_0", "<capability type='net'><interface>eth0</interface></capability>"),
|
||||
pci("0x03", "Virtio block device", "0x010000", "virtio-pci", group=2),
|
||||
child("virtio1", "pci_0000_00_03_0", "<capability type='system'/>"), # a level between, not a resource
|
||||
child("block_vda", "virtio1", "<capability type='storage'><block>/dev/vda</block></capability>"),
|
||||
pci("0x02", "Virtio 1.0 GPU", "0x030000", "virtio-pci", group=2),
|
||||
child("drm_card0", "pci_0000_00_02_0", "<devnode type='dev'>/dev/dri/card0</devnode><capability type='drm'><type>primary</type></capability>"),
|
||||
child("drm_render", "pci_0000_00_02_0", "<devnode type='dev'>/dev/dri/renderD128</devnode><capability type='drm'><type>render</type></capability>"),
|
||||
pci("0x0a", "Ethernet controller", "0x020000", "vfio-pci", group=4),
|
||||
pci("0x01", "PIIX3 USB", "0x0c0300", "uhci_hcd"),
|
||||
usb("usb_usb1", "pci_0000_00_01_0", 1, 1, "1.1 root hub", "0x1d6b", "0x0001"),
|
||||
interface("usb_1_0_1_0", "usb_usb1", 9),
|
||||
usb("usb_1_1", "usb_usb1", 1, 2),
|
||||
interface("usb_1_1_1_0", "usb_1_1", 3),
|
||||
usb("usb_1_2", "usb_usb1", 1, 3),
|
||||
interface("usb_1_2_1_0", "usb_1_2", 3),
|
||||
]
|
||||
|
||||
|
||||
def node(name, xml):
|
||||
return MagicMock(**{"name.return_value": name, "XMLDesc.return_value": xml})
|
||||
|
||||
|
||||
def vm(name, hostdevs, saved=None):
|
||||
"""A VM with hostdevs as it runs and, given saved, persistent with those for its next start."""
|
||||
def describe(flags=0):
|
||||
return f"<domain><devices>{saved if flags else hostdevs}</devices></domain>"
|
||||
return MagicMock(**{"name.return_value": name, "XMLDesc.side_effect": describe, "isPersistent.return_value": saved is not None})
|
||||
|
||||
|
||||
PCI_01 = "<hostdev mode='subsystem' type='pci'><source><address domain='0x0000' bus='0x00' slot='0x01' function='0x0'/></source></hostdev>"
|
||||
PCI_0A = "<hostdev mode='subsystem' type='pci'><source><address domain='0x0000' bus='0x00' slot='0x0a' function='0x0'/></source></hostdev>"
|
||||
USB_1_3 = "<hostdev mode='subsystem' type='usb'><source><vendor id='0x0627'/><product id='0x0001'/><address bus='1' device='3'/></source></hostdev>"
|
||||
USB_MODEL = "<hostdev mode='subsystem' type='usb'><source><vendor id='0x0627'/><product id='0x0001'/></source></hostdev>"
|
||||
|
||||
|
||||
def devices(nodes=NODES, domains=()):
|
||||
conn = wvmConnect.__new__(wvmConnect)
|
||||
conn.wvm = MagicMock(**{"listAllDevices.return_value": [node(*n) for n in nodes],
|
||||
"listAllDomains.return_value": list(domains)})
|
||||
return node_devices(conn)
|
||||
|
||||
|
||||
class NodeDevicesTests(SimpleTestCase):
|
||||
def test_pci_devices_by_group_with_what_the_host_has_on_them(self):
|
||||
pci_devices, _ = devices()
|
||||
self.assertEqual([(d["address"], d["group"]) for d in pci_devices],
|
||||
[("0000:00:02.0", 2), ("0000:00:03.0", 2), ("0000:00:06.0", 3), ("0000:00:0a.0", 4), ("0000:00:01.0", None)])
|
||||
gpu, disk, nic, spare, _ = pci_devices
|
||||
# a graphics card's primary node is the host's; its render node is not a resource of its own
|
||||
self.assertEqual((gpu["resources"], disk["resources"], nic["resources"], spare["resources"]),
|
||||
(["/dev/dri/card0"], ["/dev/vda"], ["eth0"], []))
|
||||
self.assertEqual((nic["kind"], nic["driver"], nic["ids"]), ("Network controller", "virtio-pci", "1af4:1000"))
|
||||
|
||||
def test_usb_devices_with_their_classes_hubs_and_same_models(self):
|
||||
_, (hub, tablet_2, tablet_3) = devices()
|
||||
self.assertEqual((hub["kinds"], hub["same_model"]), (["Hub"], False))
|
||||
self.assertEqual((tablet_2["kinds"], tablet_2["same_model"], tablet_3["same_model"]), (["Human interface"], True, True))
|
||||
|
||||
def test_the_vms_that_name_a_device(self):
|
||||
pci_devices, (_, tablet_2, tablet_3) = devices(domains=[vm("db-1", PCI_0A + USB_1_3), vm("kiosk-1", USB_MODEL)])
|
||||
self.assertEqual([d["vms"] for d in pci_devices], [[], [], [], ["db-1"], []])
|
||||
# by address: that device; by model only: any device of the model
|
||||
self.assertEqual((tablet_3["vms"], tablet_3["by_model"]), (["db-1"], ["kiosk-1"]))
|
||||
self.assertEqual((tablet_2["vms"], tablet_2["by_model"]), ([], ["kiosk-1"]))
|
||||
|
||||
def test_a_usb_controller_holds_the_usb_devices_below_it(self):
|
||||
pci_devices, (hub, tablet_2, _) = devices(domains=[vm("kiosk-1", USB_1_3)])
|
||||
controller = [d for d in pci_devices if d["address"] == "0000:00:01.0"][0]
|
||||
# its root hub is no device of the host's; the tablets are, one given to a VM
|
||||
self.assertEqual(controller["resources"], ["USB device 1/2 (QEMU Tablet)", "USB device 1/3 (QEMU Tablet)"])
|
||||
self.assertEqual((hub["controller"], tablet_2["controller"]), ("0000:00:01.0", "0000:00:01.0"))
|
||||
|
||||
def test_a_usb_device_on_a_controller_given_to_a_vm_is_not_given_to_another(self):
|
||||
pci_devices, usb_devices = devices(domains=[vm("db-1", PCI_01)])
|
||||
reasons = [util.render(m["text"], m["values"], str) for m in hostdev_problems(
|
||||
"kiosk-1", pci_devices, usb_devices, set(), [("0x0627", "0x0001", (1, 2))])]
|
||||
self.assertIn("USB device 1/2 (QEMU Tablet) is on the PCI controller 0000:00:01.0, given to db-1", reasons)
|
||||
|
||||
def test_a_usb_device_is_named_as_libvirt_picks_one(self):
|
||||
def named(hostdev):
|
||||
_, (_, tablet_2, tablet_3) = devices(domains=[vm("v", hostdev)])
|
||||
return [(d["vms"], d["by_model"]) for d in (tablet_2, tablet_3)]
|
||||
# an address with other IDs: another device now at that address
|
||||
other = USB_1_3.replace("0x0627", "0x046d")
|
||||
address_only = "<hostdev mode='subsystem' type='usb'><source><address bus='1' device='2'/></source></hostdev>"
|
||||
self.assertEqual(named(other), [([], []), ([], [])])
|
||||
self.assertEqual(named(address_only), [(["v"], []), ([], [])])
|
||||
self.assertEqual(named(USB_MODEL), [([], ["v"]), ([], ["v"])])
|
||||
|
||||
def test_a_pci_device_named_as_a_nic_or_only_for_the_next_start(self):
|
||||
nic = ("<interface type='hostdev'><source><address type='pci' domain='0x0000' bus='0x00' slot='0x06' function='0x0'/>"
|
||||
"</source></interface>")
|
||||
pci_devices, _ = devices(domains=[vm("nic-1", nic), vm("next-1", "", saved=PCI_0A)])
|
||||
self.assertEqual({d["address"]: d["vms"] for d in pci_devices if d["vms"]},
|
||||
{"0000:00:06.0": ["nic-1"], "0000:00:0a.0": ["next-1"]})
|
||||
|
||||
def test_sr_iov_functions_and_unnamed_hardware(self):
|
||||
vf = pci("0x0b", "", "0x020000", group=5)[1].replace(
|
||||
"<product id='0x1000'></product>", "<product id='0x10ed'/>").replace(
|
||||
"</capability></device>", "<capability type='phys_function'><address domain='0x0000' bus='0x00' slot='0x0a'"
|
||||
" function='0x0'/></capability></capability></device>")
|
||||
pf = NODES[8][1].replace("</capability></device>", "<capability type='virt_functions' maxCount='7'><address domain='0x0000'"
|
||||
" bus='0x00' slot='0x0b' function='0x0'/></capability></capability></device>")
|
||||
pci_devices, _ = devices(nodes=[("pci_0000_00_0a_0", pf), ("pci_0000_00_0b_0", vf)])
|
||||
self.assertEqual([(d["physfn"], d["vfs"], d["product"]) for d in pci_devices],
|
||||
[("", 1, "Ethernet controller"), ("0000:00:0a.0", 0, "0x10ed")])
|
||||
|
||||
def test_a_device_gone_meanwhile_is_left_out_other_errors_are_raised(self):
|
||||
for code, raised in ((libvirt.VIR_ERR_NO_NODE_DEVICE, False), (libvirt.VIR_ERR_OPERATION_FAILED, True)):
|
||||
err = libvirt.libvirtError("gone")
|
||||
err.get_error_code = lambda code=code: code
|
||||
gone = node("pci_0000_00_0b_0", "")
|
||||
gone.XMLDesc.side_effect = err
|
||||
conn = wvmConnect.__new__(wvmConnect)
|
||||
conn.wvm = MagicMock(**{"listAllDevices.return_value": [gone, node(*NODES[0])], "listAllDomains.return_value": []})
|
||||
with self.subTest(code=code):
|
||||
if raised:
|
||||
self.assertRaises(libvirt.libvirtError, node_devices, conn)
|
||||
else:
|
||||
self.assertEqual([d["address"] for d in node_devices(conn)[0]], ["0000:00:06.0"])
|
||||
|
||||
def test_a_pci_address_from_libvirts_hex_or_numbers(self):
|
||||
self.assertEqual(pci_address("0x0000", "0x00", "0x1f", "0x3"), pci_address(0, 0, 31, 3))
|
||||
self.assertEqual(pci_address(0, 0, 31, 3), "0000:00:1f.3")
|
||||
from vrtManager.tests.fakes import NODES, node
|
||||
from vrtManager.tests.fakes import PCI_0A, USB_1_3, vm
|
||||
|
||||
|
||||
class NodeDevicesPageTests(TestCase):
|
||||
|
|
|
|||
|
|
@ -1,137 +0,0 @@
|
|||
"""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.util import OperationError
|
||||
|
||||
|
||||
def device(slot, product, code, driver, group, members=None, bridge=False):
|
||||
"""A PCI device on bus 0 at slot, in IOMMU group with the slots members (its own by default)."""
|
||||
members = members or [slot]
|
||||
addresses = "".join(f"<address domain='0x0000' bus='0x00' slot='0x{m:02x}' function='0x0'/>" for m in members)
|
||||
driver_xml = f"<driver><name>{driver}</name></driver>" if driver else ""
|
||||
bridge_xml = "<capability type='pci-bridge'/>" if bridge else ""
|
||||
xml = (f"<device><name>pci_0000_00_{slot:02x}_0</name><parent>computer</parent>{driver_xml}"
|
||||
f"<capability type='pci'><class>{code}</class><domain>0</domain><bus>0</bus><slot>{slot}</slot><function>0</function>"
|
||||
f"<product id='0x0001'>{product}</product><vendor id='0x8086'>Intel</vendor>{bridge_xml}"
|
||||
f"<iommuGroup number='{group}'>{addresses}</iommuGroup></capability></device>")
|
||||
return f"pci_0000_00_{slot:02x}_0", etree.fromstring(xml)
|
||||
|
||||
|
||||
NODES = dict([
|
||||
device(0x02, "PCIe Root port", "0x060400", "pcieport", 2, bridge=True),
|
||||
device(0x03, "SATA controller", "0x010601", "ahci", 7, [0x03, 0x04, 0x05]),
|
||||
device(0x04, "SMBus controller", "0x0c0500", "i801_smbus", 7, [0x03, 0x04, 0x05]),
|
||||
device(0x05, "LPC controller", "0x060100", "lpc_ich", 7, [0x03, 0x04, 0x05]), # class 0x06, not a bridge
|
||||
device(0x06, "Gigabit NIC", "0x020000", "e1000e", 12),
|
||||
device(0x07, "Spare NIC", "0x020000", "vfio-pci", 13),
|
||||
device(0x08, "NIC port 0", "0x020000", "vfio-pci", 14, [0x08, 0x09, 0x0a]),
|
||||
device(0x09, "NIC port 1", "0x020000", None, 14, [0x08, 0x09, 0x0a]),
|
||||
device(0x0a, "Downstream port", "0x060400", "pcieport", 14, [0x08, 0x09, 0x0a], bridge=True),
|
||||
device(0x0b, "Accelerator", "0x120000", "vfio-pci", 15, [0x0b, 0x0c]), # 0x0c cannot be read
|
||||
device(0x0d, "GPU", "0x030000", "nouveau", 16, [0x0d, 0x0e]),
|
||||
device(0x0e, "GPU audio", "0x040300", "snd_hda_intel", 16, [0x0d, 0x0e]),
|
||||
])
|
||||
# what the host has below: a disk on the SATA controller, its interface on the e1000e
|
||||
NODES["block_sda"] = etree.fromstring("<device><name>block_sda</name><parent>pci_0000_00_03_0</parent>"
|
||||
"<capability type='storage'><block>/dev/sda</block></capability></device>")
|
||||
NODES["net_eth1"] = etree.fromstring("<device><name>net_eth1</name><parent>pci_0000_00_06_0</parent>"
|
||||
"<capability type='net'><interface>eth1</interface></capability></device>")
|
||||
|
||||
|
||||
def inventory(vms=()):
|
||||
"""The host's PCI devices, as a list; vms: (VM, its PCI addresses)."""
|
||||
children = {}
|
||||
for name, node in NODES.items():
|
||||
children.setdefault(node.findtext("parent"), []).append(name)
|
||||
users = [(vm, set(addresses), []) for vm, addresses in vms]
|
||||
return [_pci(name, node, NODES, children, users) for name, node in 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"<hostdev mode='subsystem' type='pci' managed='{managed}'><source>"
|
||||
f"<address domain='0x0000' bus='0x00' slot='0x{slot:02x}' function='0x0'/></source></hostdev>")
|
||||
|
||||
|
||||
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 NODES.items()]
|
||||
conn.domain_trees.return_value = []
|
||||
trees = [etree.fromstring(f"<domain><devices>{''.join(devices)}</devices></domain>") 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")])
|
||||
Loading…
Add table
Add a link
Reference in a new issue