1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-10 04:06:47 +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

@ -70,6 +70,7 @@ from vrtManager.storage import wvmStorage, wvmStorages
PERSISTENT_XML = VIR_DOMAIN_XML_INACTIVE | VIR_DOMAIN_XML_SECURE
USB_HOSTDEVS = "devices/hostdev[@mode='subsystem'][@type='usb']"
PCI_HOSTDEVS = "devices/hostdev[@mode='subsystem'][@type='pci']"
# what of the host a VM holds: host devices, and NICs that are host devices
HOST_DEVICES = "devices/hostdev | devices/interface[@type='hostdev'] | devices/interface/actual[@type='hostdev']"
@ -2389,25 +2390,31 @@ class wvmInstance(wvmConnect):
return [(element, usb_selector(mark.find("source"))) for element, mark in zip(elements, marked)]
def get_hostdevs(self):
"""The VM's USB host devices, each once: its selector (vendor,
product, address), whether the running VM has it and the device it
holds there, whether its next start has it. And its other host
devices (PCI, SCSI, mediated, NICs that are host devices), by type
and PCI address, likewise: the panel does not change them."""
usb = {}
"""The VM's host devices, each once, with whether the running VM has
it ("now") and whether its next start has it ("next"): USB devices
by selector (vendor, product, address) and the device the running
VM holds; PCI devices by address, and whether libvirt manages them
(takes them from their host drivers); the others (SCSI, mediated, NICs
that are host devices) by type and address, which the panel does
not change."""
usb, pci, others = {}, {}, {}
for xml_flags, affect in self._definitions():
where = "now" if affect == VIR_DOMAIN_AFFECT_LIVE else "next"
for element, selector in self._usb_hostdevs(xml_flags):
row = usb.setdefault(selector, {"vendor": selector[0], "product": selector[1], "address": selector[2],
"now": None, "next": False})
if affect == VIR_DOMAIN_AFFECT_LIVE:
if where == "now":
held = element.find("source/address")
row["now"] = (int(held.get("bus")), int(held.get("device"))) if held is not None else selector[2]
else:
row["next"] = True
others = {}
for xml_flags, affect in self._definitions():
tree = etree.fromstring(self._XMLDesc(xml_flags))
for device in tree.xpath("devices/hostdev[not(@type='usb')]"
for address in tree.xpath(PCI_HOSTDEVS + "/source/address"):
key = pci_address(address.get("domain", "0"), address.get("bus"), address.get("slot"), address.get("function"))
row = pci.setdefault(key, {"address": key, "now": False, "next": False, "managed": False})
row[where] = True
row["managed"] = row["managed"] or address.getparent().getparent().get("managed") == "yes"
for device in tree.xpath("devices/hostdev[not(@type='usb' or @type='pci')]"
" | devices/interface[@type='hostdev' or actual/@type='hostdev']"):
if device.tag == "interface" and device.get("type") != "hostdev":
source = device.find("actual/source") # a NIC of a network that gives host devices
@ -2423,8 +2430,42 @@ class wvmInstance(wvmConnect):
source_text = " ".join(etree.tostring(source, encoding="unicode").split()) if source is not None else ""
row = others.setdefault((kind, label or source_text),
{"type": kind, "address": label, "now": False, "next": False})
row["now" if affect == VIR_DOMAIN_AFFECT_LIVE else "next"] = True
return list(usb.values()), list(others.values())
row[where] = True
return list(usb.values()), list(pci.values()), list(others.values())
def _pci_hostdev(self, address):
"""The persistent definition's <hostdev> of the PCI device at address, or None."""
tree = etree.fromstring(self._XMLDesc(PERSISTENT_XML))
for found in tree.xpath(PCI_HOSTDEVS + "/source/address"):
if pci_address(found.get("domain", "0"), found.get("bus"), found.get("slot"), found.get("function")) == address:
return found.getparent().getparent()
return None
def _check_shut_off_for_pci(self):
"""PCI devices change in the persistent definition of a shut-off VM;
a saved state (managed save) would start the VM without the change."""
if self.instance.isActive() or self.instance.hasManagedSaveImage(0):
raise util.OperationError("Shut the VM down, without a saved state, to change its PCI devices")
def add_pci_hostdev(self, address):
"""Gives the shut-off VM the host's PCI device at address
(0000:05:00.0): libvirt binds it to vfio-pci when the VM starts and
gives it back to the host when it stops."""
self._check_shut_off_for_pci()
domain, bus, slot_function = address.split(":")
slot, function = slot_function.split(".")
hostdev = etree.Element("hostdev", mode="subsystem", type="pci", managed="yes")
etree.SubElement(etree.SubElement(hostdev, "source"), "address", domain=f"0x{domain}", bus=f"0x{bus}",
slot=f"0x{slot}", function=f"0x{function}")
self.instance.attachDeviceFlags(etree.tostring(hostdev, encoding="unicode"), VIR_DOMAIN_AFFECT_CONFIG)
def delete_pci_hostdev(self, address):
"""Takes the PCI device at address from the shut-off VM, also one the host no longer has."""
self._check_shut_off_for_pci()
hostdev = self._pci_hostdev(address)
if hostdev is None:
raise util.OperationError(f"The VM has no PCI device {address}")
self.instance.detachDeviceFlags(etree.tostring(hostdev, encoding="unicode"), VIR_DOMAIN_AFFECT_CONFIG)
def add_usb_hostdev(self, vendor, product, address=None):
"""Gives the VM the host's USB device of vendor and product, at

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)))