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:
parent
ac8ff62e7e
commit
80dd145bcf
19 changed files with 509 additions and 103 deletions
|
|
@ -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)))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue