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:
parent
ac8ff62e7e
commit
80dd145bcf
19 changed files with 509 additions and 103 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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