1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-11 04:36:48 +00:00

feat(instances): PCI devices of the host given to a shut-off VM

- The Host devices tab gives a shut-off VM a PCI device of its host and
  takes it back; libvirt binds the device to vfio-pci when the VM starts
  and gives it back when it stops (managed). The VM's PCI devices are
  listed with whether the running VM and its next start have them.
- A device is offered, and accepted on the server under the compute's
  claim, only when it is no bridge of any kind (bridge header, PCIe port,
  or of the bridge class: host bridge, LPC), the host has none of its own
  network interfaces, disks or graphics card on it (else: bind it to
  vfio-pci on the host first), no other VM names it, and every other
  endpoint of its IOMMU group is the VM's too or has no driver or
  vfio-pci and no other VM; bridges in the group stay with pcieport, and
  a member that cannot be read keeps the group. Nothing in the group is
  taken implicitly.
- A snapshot revert checks the PCI devices of the snapshot with the same
  rules; migration and clones already leave host devices out.
- The add forms of the tab load together, when the tab opens.
This commit is contained in:
catborise 2026-10-29 13:45:27 +03:00
parent ac8ff62e7e
commit 80dd145bcf
19 changed files with 509 additions and 103 deletions

View file

@ -1,5 +1,6 @@
from django.utils.translation import gettext_lazy as _
from libvirt import VIR_CONNECT_LIST_NODE_DEVICES_CAP_USB_DEV, VIR_ERR_NO_NODE_DEVICE, libvirtError
from libvirt import (VIR_CONNECT_LIST_NODE_DEVICES_CAP_PCI_DEV, VIR_CONNECT_LIST_NODE_DEVICES_CAP_USB_DEV,
VIR_ERR_NO_NODE_DEVICE, libvirtError)
from lxml import etree
from vrtManager import util
@ -40,10 +41,17 @@ def pci_address(domain, bus, slot, function):
return f"{domain:04x}:{bus:02x}:{slot:02x}.{function:x}"
def usb_hub(name, nodes, children):
"""True when the USB device named name is a hub (an interface of class 9)."""
return any(nodes[child].findtext("capability/class") == str(USB_HUB) for child in children.get(name, [])
if nodes[child].find("capability").get("type") == "usb")
def host_resources(name, nodes, children):
"""What the host itself has on the device named name or below it: its
network interfaces, block devices and graphics cards, by name. Giving
the device to a VM takes them from the host."""
network interfaces, block devices, graphics cards and USB devices (a
keyboard, or one given to a VM), by name. Giving the device to a VM
takes them from the host."""
found = []
for child in children.get(name, []):
cap = nodes[child].find("capability")
@ -53,6 +61,8 @@ def host_resources(name, nodes, children):
found.append(cap.findtext("block"))
elif cap.get("type") == "drm" and cap.findtext("type") == "primary":
found.append(nodes[child].xpath("string(devnode[@type='dev'])"))
elif cap.get("type") == "usb_device" and not usb_hub(child, nodes, children):
found.append(f"USB device {cap.findtext('bus')}/{cap.findtext('device')} ({cap.findtext('product')})")
found.extend(host_resources(child, nodes, children))
return found
@ -105,15 +115,58 @@ def device_problems(label, vms, resources, vm):
return problems
def hostdev_problems(vm, pci, usb, wanted_pci, wanted_usb):
"""Why the host's devices pci and usb (get_node_devices) that the VM
named vm wants (hostdev_users) cannot be given to it: another VM names
them too, the host has its own network interfaces, disks or graphics
card on them, or they are USB hubs."""
problems = []
def managed_pci(tree):
"""The PCI addresses a VM's definition gives it as managed host devices:
libvirt takes them from their host drivers when the VM starts."""
return {pci_address(a.get("domain", "0"), a.get("bus"), a.get("slot"), a.get("function"))
for a in tree.xpath("devices/hostdev[@mode='subsystem'][@type='pci'][@managed='yes']/source/address")}
def pci_problems(vm, device, by_address, managed):
"""Why the host's PCI device (get_node_devices) cannot be given to the VM
named vm, whose definition gives it the managed PCI devices at the
addresses managed: by_address holds every PCI device of the host. Its
IOMMU group goes to the VM as a whole, so each other endpoint in it must
be one of managed, or have no driver or vfio-pci; no other VM may have
any of them; bridges stay with the pcieport driver or none. Nothing in
the group is taken implicitly."""
label = f"PCI device {device['address']}"
# a bridge of any kind (a host bridge, a PCIe port) is the host's
if device["bridge"] or device["bridge_class"]:
return [f"{label} is a bridge"]
problems = device_problems(label, device["vms"], device["resources"], vm)
if device["resources"]:
problems.append(f"bind {device['address']} to vfio-pci on the host first if it is not needed there")
if device["group"] is None:
return problems + [f"{label} is in no IOMMU group"]
shares = f"{label} shares IOMMU group {device['group']} with"
for member in device["group_members"]:
if member == device["address"]:
continue
other = by_address.get(member)
if other is None:
problems.append(f"{shares} {member}, which cannot be read")
elif others := sorted(set(other["vms"]) - {vm}):
problems.append(f"{shares} {member} ({other['product']}), which {', '.join(others)} uses")
elif other["bridge"]:
if other["driver"] not in ("", "pcieport"):
problems.append(f"{shares} the bridge {member} (driver {other['driver']})")
elif member not in managed and other["driver"] not in ("", "vfio-pci"):
problems.append(f"{shares} {member} ({other['product']}), which the host (driver {other['driver']}) uses")
return problems
def hostdev_problems(vm, pci, usb, wanted_pci, wanted_usb, managed=frozenset()):
"""Why the host's devices pci and usb (get_node_devices) that a definition
of the VM named vm wants (hostdev_users; managed_pci) cannot be given to
it: a PCI device the host does not have, pci_problems; for USB ones,
another VM names them or their PCI controller too, the host has its own
network interfaces or disks on them, or they are hubs."""
by_address = {device["address"]: device for device in pci}
problems = [f"PCI device {address} is not on the host" for address in sorted(wanted_pci - by_address.keys())]
for device in pci:
if device["address"] in wanted_pci:
problems += device_problems(f"PCI device {device['address']}", device["vms"], device["resources"], vm)
problems += pci_problems(vm, device, by_address, managed)
for device in usb:
address = (device["bus"], device["device"])
if any(usb_named(selector, device["model"], address) for selector in wanted_usb):
@ -121,6 +174,9 @@ def hostdev_problems(vm, pci, usb, wanted_pci, wanted_usb):
problems += device_problems(label, device["vms"] + device["by_model"], device["resources"], vm)
if device["hub"]:
problems.append(f"{label} is a hub")
controller = by_address.get(device["controller"])
if controller and (holders := sorted(set(controller["vms"]) - {vm})):
problems.append(f"{label} is on the PCI controller {controller['address']}, given to {', '.join(holders)}")
return problems
@ -139,6 +195,13 @@ def _pci(name, node, nodes, children, vms):
"kind": PCI_CLASSES.get(code >> 16, _("Other")),
"driver": node.findtext("driver/name") or "",
"group": int(group) if group else None,
"group_members": [pci_address(a.get("domain"), a.get("bus"), a.get("slot"), a.get("function"))
for a in cap.xpath("iommuGroup/address")],
# a bridge header (a PCIe port too), as against class 0x06 functions such as an LPC controller
"bridge": cap.find("capability[@type='pci-bridge']") is not None
or cap.find("capability[@type='cardbus-bridge']") is not None or node.findtext("driver/name") == "pcieport",
# of the bridge class: a host bridge or an LPC controller too
"bridge_class": code >> 16 == 0x06,
"resources": host_resources(name, nodes, children),
"vms": [vm for vm, pci, _ in vms if address in pci],
"physfn": pci_address(physfn.get("domain"), physfn.get("bus"), physfn.get("slot"), physfn.get("function"))
@ -147,7 +210,6 @@ def _pci(name, node, nodes, children, vms):
}
def _usb(name, node, nodes, children, vms):
cap = node.find("capability")
bus, device = int(cap.findtext("bus")), int(cap.findtext("device"))
@ -155,6 +217,11 @@ def _usb(name, node, nodes, children, vms):
# a USB device's classes are its interfaces'
codes = [int(nodes[child].findtext("capability/class") or "0") for child in children.get(name, [])
if nodes[child].find("capability").get("type") == "usb"]
# the PCI controller it is on: a VM given that controller takes it too
parent = node.findtext("parent")
while parent in nodes and nodes[parent].find("capability").get("type") != "pci":
parent = nodes[parent].findtext("parent")
controller = nodes[parent].find("capability") if parent in nodes else None
named = [(vm, usb_named(selector, model, (bus, device))) for vm, _, selectors in vms for selector in selectors]
by_address = sorted({vm for vm, how in named if how == "address"})
return {
@ -166,6 +233,8 @@ def _usb(name, node, nodes, children, vms):
"ids": f"{model[0][2:]}:{model[1][2:]}",
"kinds": sorted({str(USB_CLASSES.get(code, _("Other"))) for code in codes}),
"hub": USB_HUB in codes,
"controller": pci_address(controller.findtext("domain"), controller.findtext("bus"), controller.findtext("slot"),
controller.findtext("function")) if controller is not None else "",
"resources": host_resources(name, nodes, children),
"vms": by_address,
# VMs that name its model only: which device of the model they take is known only when they start
@ -203,27 +272,36 @@ def node_devices(conn):
return pci, usb
def usb_names(conn):
"""The product names of the host's USB devices, by their (vendor, product)
IDs: one listing of the USB devices, one XML read each; none when they
cannot be read, as the names are only shown."""
def device_names(conn):
"""The product names of the host's USB devices, by their (vendor,
product) IDs, and of its PCI devices, by address: one listing of each,
one XML read per device; none when they cannot be read, as the names
are only shown."""
names = {}
try:
for device in conn.wvm.listAllDevices(VIR_CONNECT_LIST_NODE_DEVICES_CAP_USB_DEV):
cap = etree.fromstring(device.XMLDesc(0)).find("capability")
model = (cap.xpath("string(vendor/@id)").lower(), cap.xpath("string(product/@id)").lower())
names[model] = cap.findtext("product") or ""
except libvirtError: # a device gone meanwhile, or the host: the IDs are shown
for flag in (VIR_CONNECT_LIST_NODE_DEVICES_CAP_USB_DEV, VIR_CONNECT_LIST_NODE_DEVICES_CAP_PCI_DEV):
for device in conn.wvm.listAllDevices(flag):
cap = etree.fromstring(device.XMLDesc(0)).find("capability")
if cap.get("type") == "pci":
key = pci_address(cap.findtext("domain"), cap.findtext("bus"), cap.findtext("slot"), cap.findtext("function"))
else:
key = (cap.xpath("string(vendor/@id)").lower(), cap.xpath("string(product/@id)").lower())
names[key] = cap.findtext("product") or ""
except libvirtError: # a device gone meanwhile, or the host: the IDs and addresses are shown
return {}
return names
def check_hostdevs(conn, vm, trees):
"""Refuses the host devices the definitions trees give the VM named vm,
on the host of conn, when they cannot be (hostdev_problems)."""
wanted = hostdev_users(trees)
if not any(wanted):
on the host of conn, when they cannot be (hostdev_problems): each
definition on its own, as the VM starts from one of them."""
wanted = [(tree, hostdev_users([tree])) for tree in trees]
if not any(pci or usb for _, (pci, usb) in wanted):
return
pci, usb = node_devices(conn)
if problems := hostdev_problems(vm, pci, usb, *wanted):
raise util.OperationError("; ".join(problems))
problems = []
for tree, (wanted_pci, wanted_usb) in wanted:
problems += hostdev_problems(vm, pci, usb, wanted_pci, wanted_usb, managed_pci(tree))
if problems:
raise util.OperationError("; ".join(dict.fromkeys(problems)))