On 3/17/25 21:59, Maximilian Martin via Devel wrote: > From: Maximilian Martin <maximilian_mar...@gmx.de> > > Currently, only vendor/product and bus/device matching are supported for USB > host > devices. Neither of these provide a stable and persistent way of assigning a > guest > a specific host device. Vendor/product can be ambiguous. Device numbers > change on > every enumeration. > > This patch adds a bus/port matching, which allows a specific port on the host > to be > specified using the dotted notation found in Linux's "devpath" sysfs > attribute. > > This patch is based on the previous work of Thomas Hebb: > https://lists.libvirt.org/archives/list/devel@lists.libvirt.org/message/7U3HFUW3DGDOSF4RIBRZJINKFDYCE2ZH/ > > Resolves: https://gitlab.com/libvirt/libvirt/-/issues/513 > > Signed-off-by: Maximilian Martin <maximilian_mar...@gmx.de> > ---
There's too much going on in a single patch. Please split the change into multiple patches. > docs/formatdomain.rst | 29 ++-- > src/conf/domain_conf.c | 70 +++++++- > src/conf/domain_conf.h | 1 + > src/conf/schemas/domaincommon.rng | 11 +- > src/hypervisor/virhostdev.c | 131 +++++++++------ > src/libvirt_private.syms | 2 - > src/util/virusb.c | 159 ++++++------------ > src/util/virusb.h | 32 ++-- > tests/virusbtest.c | 149 +++++++++++----- > .../sys_bus_usb/devices/1-1.5.3.1/devpath | 1 + > .../sys_bus_usb/devices/1-1.5.3.3/devpath | 1 + > .../sys_bus_usb/devices/1-1.5.3/devpath | 1 + > .../sys_bus_usb/devices/1-1.5.4/devpath | 1 + > .../sys_bus_usb/devices/1-1.5.5/devpath | 1 + > .../sys_bus_usb/devices/1-1.5.6/devpath | 1 + > .../sys_bus_usb/devices/1-1.5/devpath | 1 + > .../sys_bus_usb/devices/1-1.6/devpath | 1 + > .../sys_bus_usb/devices/1-1/devpath | 1 + > .../sys_bus_usb/devices/2-1.2/devpath | 1 + > .../sys_bus_usb/devices/2-1/devpath | 1 + > .../sys_bus_usb/devices/usb1/devpath | 1 + > .../sys_bus_usb/devices/usb2/devpath | 1 + > .../sys_bus_usb/devices/usb3/devpath | 1 + > .../sys_bus_usb/devices/usb4/devpath | 1 + > 24 files changed, 355 insertions(+), 244 deletions(-) > create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/1-1.5.3.1/devpath > create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/1-1.5.3.3/devpath > create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/1-1.5.3/devpath > create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/1-1.5.4/devpath > create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/1-1.5.5/devpath > create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/1-1.5.6/devpath > create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/1-1.5/devpath > create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/1-1.6/devpath > create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/1-1/devpath > create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/2-1.2/devpath > create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/2-1/devpath > create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/usb1/devpath > create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/usb2/devpath > create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/usb3/devpath > create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/usb4/devpath > > diff --git a/docs/formatdomain.rst b/docs/formatdomain.rst > index 543b36cb2e..bc3c11613f 100644 > --- a/docs/formatdomain.rst > +++ b/docs/formatdomain.rst > @@ -4696,19 +4696,22 @@ or: > tweak the loading process further using the ``bar`` or ``file`` attributes > will be rejected. :since:`Since 4.3.0 (QEMU and KVM only)`. > ``address`` > - The ``address`` element for USB devices has a ``bus`` and ``device`` > - attribute to specify the USB bus and device number the device appears at > on > - the host. The values of these attributes can be given in decimal, > hexadecimal > - (starting with 0x) or octal (starting with 0) form. For PCI devices the > - element carries 4 attributes allowing to designate the device as can be > found > - with the ``lspci`` or with ``virsh nodedev-list``. For SCSI devices a > 'drive' > - address type must be used. For mediated devices, which are software-only > - devices defining an allocation of resources on the physical parent device, > - the address type used must conform to the ``model`` attribute of element > - ``hostdev``, e.g. any address type other than PCI for ``vfio-pci`` device > API > - or any address type other than CCW for ``vfio-ccw`` device API will > result in > - an error. See the `Device Addresses`_ section for more details on the > address > - element. > + The ``address`` element for USB devices has a ``bus`` attribute to specify > + the USB bus. In addition, either a ``device`` attribute or a ``port`` > + attribute is required to identify the device on the host. While the device > + number is assigned upon connection of the device, the port number is a > + stable identifier of the physical host port. Bus and device number can be > + given in decimal, hexadecimal (starting with 0x) or octal (starting with > 0) > + form. The port number is a dotted path (examples: ``2``, ``1.2.5``). For > PCI > + devices the element carries 4 attributes allowing to designate the device > as > + can be found with the ``lspci`` or with ``virsh nodedev-list``. For SCSI > + devices a 'drive' address type must be used. For mediated devices, which > are > + software-only devices defining an allocation of resources on the physical > + parent device, the address type used must conform to the ``model`` > attribute > + of element ``hostdev``, e.g. any address type other than PCI for > ``vfio-pci`` > + device API or any address type other than CCW for ``vfio-ccw`` device API > + will result in an error. See the `Device Addresses`_ section for more > details > + on the address element. > ``driver`` > PCI hostdev devices can have an optional ``driver`` subelement that > specifies which host driver to bind to the device when preparing it > diff --git a/src/conf/domain_conf.c b/src/conf/domain_conf.c > index b94cf99236..bd9c651a07 100644 > --- a/src/conf/domain_conf.c > +++ b/src/conf/domain_conf.c > @@ -2657,6 +2657,15 @@ > virDomainHostdevSubsysSCSIClear(virDomainHostdevSubsysSCSI *scsisrc) > } > } > > +static void > +virDomainHostdevSubsysUSBClear(virDomainHostdevSubsysUSB *usbsrc) > +{ > + if (!usbsrc) > + return; > + > + VIR_FREE(usbsrc->port); > +} > + > > static void > virDomainHostdevDefClear(virDomainHostdevDef *def) > @@ -2700,6 +2709,8 @@ virDomainHostdevDefClear(virDomainHostdevDef *def) > g_clear_pointer(&def->source.subsys.u.pci.origstates, > virBitmapFree); > break; > case VIR_DOMAIN_HOSTDEV_SUBSYS_TYPE_USB: > + virDomainHostdevSubsysUSBClear(&def->source.subsys.u.usb); > + break; > case VIR_DOMAIN_HOSTDEV_SUBSYS_TYPE_MDEV: > case VIR_DOMAIN_HOSTDEV_SUBSYS_TYPE_LAST: > break; > @@ -5841,6 +5852,9 @@ virDomainHostdevSubsysUSBDefParseXML(xmlNodePtr node, > xmlNodePtr productNode; > xmlNodePtr addressNode; > virTristateBool autoAddress; > + bool found_device, found_port; > + char *port; > + int ret = -1; These variables are only used ... > VIR_XPATH_NODE_AUTORESTORE(ctxt) > > ctxt->node = node; > @@ -5891,13 +5905,45 @@ virDomainHostdevSubsysUSBDefParseXML(xmlNodePtr node, > } > > if ((addressNode = virXPathNode("./address", ctxt))) { > - if (virXMLPropUInt(addressNode, "bus", 0, > - VIR_XML_PROP_REQUIRED, &usbsrc->bus) < 0) ... inside of this block. Declare them in it please. > + found_device = false; > + found_port = false; > + > + ret = virXMLPropUInt(addressNode, "bus", 0, > + VIR_XML_PROP_REQUIRED, &usbsrc->bus); > + if (ret < 0) { > + return -1; > + } else if (ret == 0) { > + virReportError(VIR_ERR_INTERNAL_ERROR, > + "%s", _("missing bus")); > + return -1; > + } > + > + ret = virXMLPropUInt(addressNode, "device", 0, > + VIR_XML_PROP_NONE, &usbsrc->device); > + if (ret < 0) > return -1; > + else if (ret > 0) > + found_device = true; > > - if (virXMLPropUInt(addressNode, "device", 0, > - VIR_XML_PROP_REQUIRED, &usbsrc->device) < 0) > + port = virXMLPropString(addressNode, "port"); > + if (port) { > + if (*port) { > + usbsrc->port = port; > + found_port = true; > + } else { > + VIR_FREE(port); > + } > + } > + > + if (!found_device && !found_port) { > + virReportError(VIR_ERR_INTERNAL_ERROR, > + "%s", _("usb address needs either device id or port")); > return -1; > + } else if (found_device && found_port) { > + virReportError(VIR_ERR_INTERNAL_ERROR, > + "%s", _("found both device id and port in usb address > (ambiguous setting)")); > + return -1; > + } > } > > return 0; > @@ -14522,8 +14568,13 @@ virDomainHostdevMatchSubsysUSB(virDomainHostdevDef > *first, > virDomainHostdevSubsysUSB *first_usbsrc = &first->source.subsys.u.usb; > virDomainHostdevSubsysUSB *second_usbsrc = &second->source.subsys.u.usb; > > - if (first_usbsrc->bus && first_usbsrc->device) { > - /* specified by bus location on host */ > + if (first_usbsrc->bus && first_usbsrc->port) { > + /* specified by bus and port on host */ > + if (first_usbsrc->bus == second_usbsrc->bus && > + STREQ_NULLABLE(first_usbsrc->port, second_usbsrc->port)) > + return 1; > + } else if (first_usbsrc->bus && first_usbsrc->device) { > + /* specified by bus and device id on host */ > if (first_usbsrc->bus == second_usbsrc->bus && > first_usbsrc->device == second_usbsrc->device) > return 1; > @@ -23957,10 +24008,15 @@ virDomainHostdevDefFormatSubsysUSB(virBuffer *buf, > virBufferAsprintf(&sourceChildBuf, "<product id='0x%.4x'/>\n", > usbsrc->product); > } > > - if (usbsrc->bus || usbsrc->device) > + if (usbsrc->bus && usbsrc->port) { > + virBufferAsprintf(&sourceChildBuf, "<address %sbus='%d' > port='%s'/>\n", > + includeTypeInAddr ? "type='usb' " : "", > + usbsrc->bus, usbsrc->port); > + } else if (usbsrc->bus || usbsrc->device) { > virBufferAsprintf(&sourceChildBuf, "<address %sbus='%d' > device='%d'/>\n", > includeTypeInAddr ? "type='usb' " : "", > usbsrc->bus, usbsrc->device); > + } > > virXMLFormatElement(buf, "source", &sourceAttrBuf, &sourceChildBuf); > } > diff --git a/src/conf/domain_conf.h b/src/conf/domain_conf.h > index 3a97fd866c..e5f53c09ac 100644 > --- a/src/conf/domain_conf.h > +++ b/src/conf/domain_conf.h > @@ -228,6 +228,7 @@ struct _virDomainHostdevSubsysUSB { > on vendor/product */ > unsigned bus; > unsigned device; > + char *port; > > unsigned vendor; > unsigned product; > diff --git a/src/conf/schemas/domaincommon.rng > b/src/conf/schemas/domaincommon.rng > index 3276569325..00d97f0cd8 100644 > --- a/src/conf/schemas/domaincommon.rng > +++ b/src/conf/schemas/domaincommon.rng > @@ -6687,9 +6687,14 @@ > <attribute name="bus"> > <ref name="usbAddr"/> > </attribute> > - <attribute name="device"> > - <ref name="usbAddr"/> > - </attribute> > + <choice> > + <attribute name="device"> > + <ref name="usbAddr"/> > + </attribute> > + <attribute name="port"> > + <ref name="usbPort"/> > + </attribute> > + </choice> > </element> > </define> > <define name="scsiaddress"> > diff --git a/src/hypervisor/virhostdev.c b/src/hypervisor/virhostdev.c > index 0a1d8500d4..9e77477a9b 100644 > --- a/src/hypervisor/virhostdev.c > +++ b/src/hypervisor/virhostdev.c > @@ -1358,6 +1358,42 @@ virHostdevMarkUSBDevices(virHostdevManager *mgr, > return -1; > } > > +static int > +virHostdevFindUSBDeviceWithFlags(virDomainHostdevDef *hostdev, > + bool mandatory, > + unsigned int flags, > + virUSBDevice **usb) > +{ > + virDomainHostdevSubsysUSB *usbsrc = &hostdev->source.subsys.u.usb; > + unsigned vendor = usbsrc->vendor; > + unsigned product = usbsrc->product; > + unsigned bus = usbsrc->bus; > + char *port = usbsrc->port; > + unsigned device = usbsrc->device; > + virUSBDeviceList *devs; > + int rc; > + > + rc = virUSBDeviceFind(vendor, product, bus, device, port, NULL, > + mandatory, flags, &devs); > + if (rc < 0) > + return -1; > + > + if (rc == 1) { > + *usb = virUSBDeviceListGet(devs, 0); > + virUSBDeviceListSteal(devs, *usb); > + } > + virObjectUnref(devs); > + > + if (rc > 1) { > + virReportError(VIR_ERR_OPERATION_FAILED, > + _("Multiple USB devices for %1$x:%2$x, use <address> > to specify one"), > + vendor, product); > + return -1; > + } > + > + return rc; > +} > + > > int > virHostdevFindUSBDevice(virDomainHostdevDef *hostdev, > @@ -1366,77 +1402,64 @@ virHostdevFindUSBDevice(virDomainHostdevDef *hostdev, > { > virDomainHostdevSubsysUSB *usbsrc = &hostdev->source.subsys.u.usb; > unsigned vendor = usbsrc->vendor; > - unsigned product = usbsrc->product; > unsigned bus = usbsrc->bus; > unsigned device = usbsrc->device; > + char *port = usbsrc->port; > bool autoAddress = usbsrc->autoAddress; > + unsigned int flags = 0; > int rc; > > *usb = NULL; > > - if (vendor && bus) { > - rc = virUSBDeviceFind(vendor, product, bus, device, > - NULL, > - autoAddress ? false : mandatory, > - usb); > - if (rc < 0) { > - return -1; > - } else if (!autoAddress) { > - goto out; > - } else { > - VIR_INFO("USB device %x:%x could not be found at previous" > - " address (bus:%u device:%u)", > - vendor, product, bus, device); > - } > + if (vendor) > + flags |= USB_DEVICE_FIND_BY_VENDOR; > + if (device) > + flags |= USB_DEVICE_FIND_BY_DEVICE; > + if (port) > + flags |= USB_DEVICE_FIND_BY_PORT; > + > + /* Rule out invalid cases. */ > + if (vendor && device && port) { > + VIR_WARN("Cannot match USB device on vendor/product, bus/device, and > bus/port at once. Ignoring bus/device."); > + flags &= ~((unsigned int) USB_DEVICE_FIND_BY_DEVICE); > + } else if (device && port) { > + VIR_WARN("Cannot match USB device on bus/device and bus/port at > once. Ignoring bus/device."); > + flags &= ~((unsigned int) USB_DEVICE_FIND_BY_DEVICE); > + } else if (!vendor && !device && !port) { > + VIR_WARN("No matching fields for USB device found. Vendor/product, > bus/device, or bus/port required."); > + return -1; > } > > - /* When vendor is specified, its USB address is either unspecified or the > - * device could not be found at the USB device where it had been > - * automatically found before. > - */ > - if (vendor) { > - g_autoptr(virUSBDeviceList) devs = NULL; > + /* First attempt, matching on all valid fields. */ > + rc = virHostdevFindUSBDeviceWithFlags(hostdev, > + autoAddress ? false : mandatory, > + flags, usb); > + if (rc < 0) > + return -1; > > - rc = virUSBDeviceFindByVendor(vendor, product, NULL, mandatory, > &devs); > - if (rc < 0) { > - return -1; > - } else if (rc == 0) { > - goto out; > - } else if (rc > 1) { > - if (autoAddress) { > - virReportError(VIR_ERR_OPERATION_FAILED, > - _("Multiple USB devices for %1$x:%2$x were > found, but none of them is at bus:%3$u device:%4$u"), > - vendor, product, bus, device); > - } else { > - virReportError(VIR_ERR_OPERATION_FAILED, > - _("Multiple USB devices for %1$x:%2$x, use > <address> to specify one"), > - vendor, product); > - } > + if (rc != 1 && autoAddress && device) { > + VIR_INFO("USB device could not be found at previous address " > + "(bus:%u device:%u)", bus, device); > + > + /* Second attempt, for when the device number has changed. */ > + flags &= ~((unsigned int) USB_DEVICE_FIND_BY_DEVICE); > + usbsrc->device = 0; > + > + rc = virHostdevFindUSBDeviceWithFlags(hostdev, mandatory, > + flags, usb); > + if (rc < 0) > return -1; > - } > > - *usb = virUSBDeviceListGet(devs, 0); > - virUSBDeviceListSteal(devs, *usb); > + usbsrc->autoAddress = true; > + } > > + if (!*usb) { > + hostdev->missing = true; > + } else if (!usbsrc->bus || !usbsrc->device) { > usbsrc->bus = virUSBDeviceGetBus(*usb); > usbsrc->device = virUSBDeviceGetDevno(*usb); > - usbsrc->autoAddress = true; > - > - if (autoAddress) { > - VIR_INFO("USB device %x:%x found at bus:%u device:%u (moved" > - " from bus:%u device:%u)", > - vendor, product, > - usbsrc->bus, usbsrc->device, > - bus, device); > - } > - } else if (bus) { > - if (virUSBDeviceFindByBus(bus, device, NULL, mandatory, usb) < 0) > - return -1; > } > > - out: > - if (!*usb) > - hostdev->missing = true; > return 0; > } > > diff --git a/src/libvirt_private.syms b/src/libvirt_private.syms > index e63939e2b5..dbc0778fdb 100644 > --- a/src/libvirt_private.syms > +++ b/src/libvirt_private.syms > @@ -3668,8 +3668,6 @@ virURIResolveAlias; > # util/virusb.h > virUSBDeviceFileIterate; > virUSBDeviceFind; > -virUSBDeviceFindByBus; > -virUSBDeviceFindByVendor; > virUSBDeviceFree; > virUSBDeviceGetBus; > virUSBDeviceGetDevno; > diff --git a/src/util/virusb.c b/src/util/virusb.c > index cf3461f80a..d193f8f3ba 100644 > --- a/src/util/virusb.c > +++ b/src/util/virusb.c > @@ -61,12 +61,6 @@ struct _virUSBDeviceList { > virUSBDevice **devs; > }; > > -typedef enum { > - USB_DEVICE_ALL = 0, > - USB_DEVICE_FIND_BY_VENDOR = 1 << 0, > - USB_DEVICE_FIND_BY_BUS = 1 << 1, > -} virUSBDeviceFindFlags; > - > static virClass *virUSBDeviceListClass; > > static void virUSBDeviceListDispose(void *obj); > @@ -102,11 +96,27 @@ static int virUSBSysReadFile(const char *f_name, const > char *d_name, > return 0; > } > > +static int virUSBSysReadFileStr(const char *f_name, const char *d_name, > + char **value) > +{ > + char *buf = NULL; > + g_autofree char *filename = NULL; > + > + filename = g_strdup_printf(USB_SYSFS "/devices/%s/%s", d_name, f_name); > + > + if (virFileReadAll(filename, 1024, &buf) < 0) > + return -1; > + > + *value = buf; > + return 0; > +} > + > static virUSBDeviceList * > virUSBDeviceSearch(unsigned int vendor, > unsigned int product, > unsigned int bus, > unsigned int devno, > + const char *port, > const char *vroot, > unsigned int flags) > { > @@ -127,6 +137,8 @@ virUSBDeviceSearch(unsigned int vendor, > > while ((direrr = virDirRead(dir, &de, USB_SYSFS "/devices")) > 0) { > unsigned int found_prod, found_vend, found_bus, found_devno; > + char *found_port; > + bool port_matches; > char *tmpstr = de->d_name; > > if (strchr(de->d_name, ':')) > @@ -154,16 +166,32 @@ virUSBDeviceSearch(unsigned int vendor, > 10, &found_devno) < 0) > goto cleanup; > > - if ((flags & USB_DEVICE_FIND_BY_VENDOR) && > - (found_prod != product || found_vend != vendor)) > - continue; > + if (virUSBSysReadFileStr("devpath", de->d_name, > + &found_port) < 0) { > + goto cleanup; > + } else { > + found_port[strlen(found_port) - 1] = '\0'; /* remove newline */ > + port_matches = STREQ_NULLABLE(found_port, port); > + VIR_FREE(found_port); > + } > > - if (flags & USB_DEVICE_FIND_BY_BUS) { > + if (flags & USB_DEVICE_FIND_BY_VENDOR) { > + if (found_prod != product || found_vend != vendor) > + continue; > + } > + > + if (flags & USB_DEVICE_FIND_BY_DEVICE) { > if (found_bus != bus || found_devno != devno) > continue; > found = true; > } > > + if (flags & USB_DEVICE_FIND_BY_PORT) { > + if (found_bus != bus || !port_matches) > + continue; > + found = true; > + } > + > usb = virUSBDeviceNew(found_bus, found_devno, vroot); > > if (!usb) > @@ -186,36 +214,41 @@ virUSBDeviceSearch(unsigned int vendor, > } > > int > -virUSBDeviceFindByVendor(unsigned int vendor, > - unsigned int product, > - const char *vroot, > - bool mandatory, > - virUSBDeviceList **devices) > +virUSBDeviceFind(unsigned int vendor, > + unsigned int product, > + unsigned int bus, > + unsigned int devno, > + const char *port, > + const char *vroot, > + bool mandatory, > + unsigned int flags, > + virUSBDeviceList **devices) > { > virUSBDeviceList *list; > int count; > > - if (!(list = virUSBDeviceSearch(vendor, product, 0, 0, > - vroot, > - USB_DEVICE_FIND_BY_VENDOR))) > + if (!(list = virUSBDeviceSearch(vendor, product, bus, devno, port, > + vroot, flags))) > return -1; > > - if (list->count == 0) { > + count = list->count; > + if (count == 0) { > virObjectUnref(list); > if (!mandatory) { > - VIR_DEBUG("Did not find USB device %04x:%04x", > - vendor, product); > if (devices) > *devices = NULL; > return 0; > } > > virReportError(VIR_ERR_INTERNAL_ERROR, > - _("Did not find USB device %1$04x:%2$04x"), vendor, > product); > + _("Did not find matching USB device (matching on%1$s%2$s%3$s > others are ignored): vid:%4$04x, pid:%5$04x, bus:%6$u, device:%7$u, > port:%8$s"), > + flags & USB_DEVICE_FIND_BY_VENDOR ? " vid/pid," : "", > + flags & USB_DEVICE_FIND_BY_DEVICE ? " bus/device," : "", > + flags & USB_DEVICE_FIND_BY_PORT ? " bus/port," : "", > + vendor, product, bus, devno, port ? port : "(null)"); This is going to be horrible for translators. > return -1; > } > > - count = list->count; > if (devices) > *devices = list; > else Michal