[PATCH] USB hostdev: allow addressing by port

Maximilian Martin via Devel posted 1 patch 1 year ago
Patches applied successfully (tree, apply log)
git fetch https://github.com/patchew-project/libvirt tags/patchew/20250317205954.1298-1-maximilian._5Fmartin@gmx.de
There is a newer version of this series
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
[PATCH] USB hostdev: allow addressing by port
Posted by Maximilian Martin via Devel 1 year ago
From: Maximilian Martin <maximilian_martin@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_martin@gmx.de>
---
 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;
     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)
+        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)");
         return -1;
     }

-    count = list->count;
     if (devices)
         *devices = list;
     else
@@ -224,86 +257,6 @@ virUSBDeviceFindByVendor(unsigned int vendor,
     return count;
 }

-int
-virUSBDeviceFindByBus(unsigned int bus,
-                      unsigned int devno,
-                      const char *vroot,
-                      bool mandatory,
-                      virUSBDevice **usb)
-{
-    virUSBDeviceList *list;
-
-    if (!(list = virUSBDeviceSearch(0, 0, bus, devno,
-                                    vroot,
-                                    USB_DEVICE_FIND_BY_BUS)))
-        return -1;
-
-    if (list->count == 0) {
-        virObjectUnref(list);
-        if (!mandatory) {
-            VIR_DEBUG("Did not find USB device bus:%u device:%u",
-                      bus, devno);
-            if (usb)
-                *usb = NULL;
-            return 0;
-        }
-
-        virReportError(VIR_ERR_INTERNAL_ERROR,
-                       _("Did not find USB device bus:%1$u device:%2$u"),
-                       bus, devno);
-        return -1;
-    }
-
-    if (usb) {
-        *usb = virUSBDeviceListGet(list, 0);
-        virUSBDeviceListSteal(list, *usb);
-    }
-    virObjectUnref(list);
-
-    return 0;
-}
-
-int
-virUSBDeviceFind(unsigned int vendor,
-                 unsigned int product,
-                 unsigned int bus,
-                 unsigned int devno,
-                 const char *vroot,
-                 bool mandatory,
-                 virUSBDevice **usb)
-{
-    virUSBDeviceList *list;
-
-    unsigned int flags = USB_DEVICE_FIND_BY_VENDOR|USB_DEVICE_FIND_BY_BUS;
-    if (!(list = virUSBDeviceSearch(vendor, product, bus, devno,
-                                    vroot, flags)))
-        return -1;
-
-    if (list->count == 0) {
-        virObjectUnref(list);
-        if (!mandatory) {
-            VIR_DEBUG("Did not find USB device %04x:%04x bus:%u device:%u",
-                      vendor, product, bus, devno);
-            if (usb)
-                *usb = NULL;
-            return 0;
-        }
-
-        virReportError(VIR_ERR_INTERNAL_ERROR,
-                       _("Did not find USB device %1$04x:%2$04x bus:%3$u device:%4$u"),
-                       vendor, product, bus, devno);
-        return -1;
-    }
-
-    if (usb) {
-        *usb = virUSBDeviceListGet(list, 0);
-        virUSBDeviceListSteal(list, *usb);
-    }
-    virObjectUnref(list);
-
-    return 0;
-}
-
 virUSBDevice *
 virUSBDeviceNew(unsigned int bus,
                 unsigned int devno,
diff --git a/src/util/virusb.h b/src/util/virusb.h
index d2b3f69942..cfd1ad51cc 100644
--- a/src/util/virusb.h
+++ b/src/util/virusb.h
@@ -30,30 +30,26 @@ typedef struct _virUSBDeviceList virUSBDeviceList;

 G_DEFINE_AUTOPTR_CLEANUP_FUNC(virUSBDeviceList, virObjectUnref);

+typedef enum {
+    USB_DEVICE_ALL = 0,
+    USB_DEVICE_FIND_BY_VENDOR = 1 << 0,
+    USB_DEVICE_FIND_BY_DEVICE = 1 << 1,
+    USB_DEVICE_FIND_BY_PORT = 1 << 2,
+} virUSBDeviceFindFlags;

 virUSBDevice *virUSBDeviceNew(unsigned int bus,
                                 unsigned int devno,
                                 const char *vroot);

-int virUSBDeviceFindByBus(unsigned int bus,
-                          unsigned int devno,
-                          const char *vroot,
-                          bool mandatory,
-                          virUSBDevice **usb);
-
-int virUSBDeviceFindByVendor(unsigned int vendor,
-                             unsigned int product,
-                             const char *vroot,
-                             bool mandatory,
-                             virUSBDeviceList **devices);
-
 int virUSBDeviceFind(unsigned int vendor,
-                     unsigned int product,
-                     unsigned int bus,
-                     unsigned int devno,
-                     const char *vroot,
-                     bool mandatory,
-                     virUSBDevice **usb);
+                 unsigned int product,
+                 unsigned int bus,
+                 unsigned int devno,
+                 const char *port,
+                 const char *vroot,
+                 bool mandatory,
+                 unsigned int flags,
+                 virUSBDeviceList **devices);

 void virUSBDeviceFree(virUSBDevice *dev);
 int virUSBDeviceSetUsedBy(virUSBDevice *dev,
diff --git a/tests/virusbtest.c b/tests/virusbtest.c
index 870e136321..62b7b8bc29 100644
--- a/tests/virusbtest.c
+++ b/tests/virusbtest.c
@@ -26,9 +26,11 @@
 #define VIR_FROM_THIS VIR_FROM_NONE

 typedef enum {
-    FIND_BY_ALL,
     FIND_BY_VENDOR,
-    FIND_BY_BUS
+    FIND_BY_DEVICE,
+    FIND_BY_PORT,
+    FIND_BY_VENDOR_AND_DEVICE,
+    FIND_BY_VENDOR_AND_PORT
 } testUSBFindFlags;

 struct findTestInfo {
@@ -37,6 +39,7 @@ struct findTestInfo {
     unsigned int product;
     unsigned int bus;
     unsigned int devno;
+    const char *port;
     const char *vroot;
     bool mandatory;
     int how;
@@ -70,25 +73,34 @@ static int testDeviceFind(const void *opaque)
     g_autoptr(virUSBDeviceList) devs = NULL;
     int rv = 0;
     size_t i, ndevs = 0;
+    unsigned int flags = 0;

     switch (info->how) {
-    case FIND_BY_ALL:
-        rv = virUSBDeviceFind(info->vendor, info->product,
-                              info->bus, info->devno,
-                              info->vroot, info->mandatory, &dev);
-        break;
     case FIND_BY_VENDOR:
-        rv = virUSBDeviceFindByVendor(info->vendor, info->product,
-                                      info->vroot, info->mandatory, &devs);
+        flags = USB_DEVICE_FIND_BY_VENDOR;
+        break;
+    case FIND_BY_DEVICE:
+        flags = USB_DEVICE_FIND_BY_DEVICE;
+        break;
+    case FIND_BY_PORT:
+        flags = USB_DEVICE_FIND_BY_PORT;
         break;
-    case FIND_BY_BUS:
-        rv = virUSBDeviceFindByBus(info->bus, info->devno,
-                                   info->vroot, info->mandatory, &dev);
+    case FIND_BY_VENDOR_AND_DEVICE:
+        flags = USB_DEVICE_FIND_BY_VENDOR |
+                USB_DEVICE_FIND_BY_DEVICE;
+        break;
+    case FIND_BY_VENDOR_AND_PORT:
+        flags = USB_DEVICE_FIND_BY_VENDOR |
+                USB_DEVICE_FIND_BY_PORT;
         break;
     }

+    rv = virUSBDeviceFind(info->vendor, info->product,
+        info->bus, info->devno, info->port,
+        info->vroot, info->mandatory, flags, &devs);
+
     if (info->expectFailure) {
-        if (rv == 0) {
+        if (rv >= 0) {
             virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
                            "unexpected success");
         } else {
@@ -99,9 +111,20 @@ static int testDeviceFind(const void *opaque)
         goto cleanup;
     }

+    if (info->how != FIND_BY_VENDOR) {
+        if (rv == 1) {
+            dev = virUSBDeviceListGet(devs, 0);
+            virUSBDeviceListSteal(devs, dev);
+        } else {
+            goto cleanup;
+        }
+    }
+
     switch (info->how) {
-    case FIND_BY_ALL:
-    case FIND_BY_BUS:
+    case FIND_BY_DEVICE:
+    case FIND_BY_PORT:
+    case FIND_BY_VENDOR_AND_DEVICE:
+    case FIND_BY_VENDOR_AND_PORT:
         if (virUSBDeviceFileIterate(dev, testDeviceFileActor, NULL) < 0)
             goto cleanup;
         break;
@@ -146,14 +169,17 @@ testUSBList(const void *opaque G_GNUC_UNUSED)
     virUSBDeviceList *list = NULL;
     virUSBDeviceList *devlist = NULL;
     virUSBDevice *dev = NULL;
+    virUSBDeviceList *devs = NULL;
     int ret = -1;
+    int rv;
     size_t i, ndevs;

     if (!(list = virUSBDeviceListNew()))
         goto cleanup;

 #define EXPECTED_NDEVS_ONE 3
-    if (virUSBDeviceFindByVendor(0x1d6b, 0x0002, NULL, true, &devlist) < 0)
+    if (virUSBDeviceFind(0x1d6b, 0x0002, 0, 0, NULL, NULL, true,
+                         USB_DEVICE_FIND_BY_VENDOR, &devlist) < 0)
         goto cleanup;

     ndevs = virUSBDeviceListCount(devlist);
@@ -176,7 +202,8 @@ testUSBList(const void *opaque G_GNUC_UNUSED)
         goto cleanup;

 #define EXPECTED_NDEVS_TWO 3
-    if (virUSBDeviceFindByVendor(0x18d1, 0x4e22, NULL, true, &devlist) < 0)
+    if (virUSBDeviceFind(0x18d1, 0x4e22, 0, 0, NULL, NULL, true,
+                         USB_DEVICE_FIND_BY_VENDOR, &devlist) < 0)
         goto cleanup;

     ndevs = virUSBDeviceListCount(devlist);
@@ -196,8 +223,16 @@ testUSBList(const void *opaque G_GNUC_UNUSED)
                        EXPECTED_NDEVS_ONE + EXPECTED_NDEVS_TWO) < 0)
         goto cleanup;

-    if (virUSBDeviceFind(0x18d1, 0x4e22, 1, 20, NULL, true, &dev) < 0)
+    rv = virUSBDeviceFind(0x18d1, 0x4e22, 1, 20, NULL, NULL, true,
+                            USB_DEVICE_FIND_BY_VENDOR |
+                            USB_DEVICE_FIND_BY_DEVICE, &devs);
+    if (rv != 1) {
         goto cleanup;
+    } else {
+        dev = virUSBDeviceListGet(devs, 0);
+        virUSBDeviceListSteal(devs, dev);
+    }
+    virObjectUnref(devs);

     if (!virUSBDeviceListFind(list, dev)) {
         virReportError(VIR_ERR_INTERNAL_ERROR,
@@ -229,49 +264,75 @@ mymain(void)
 {
     int rv = 0;

-#define DO_TEST_FIND_FULL(name, vend, prod, bus, devno, vroot, mand, how, fail) \
+#define DO_TEST_FIND_FULL(name, vend, prod, bus, devno, \
+                          port, vroot, mand, how, fail) \
     do { \
         struct findTestInfo data = { name, vend, prod, bus, \
-            devno, vroot, mand, how, fail \
+            devno, port, vroot, mand, how, fail \
         }; \
         if (virTestRun("USBDeviceFind " name, testDeviceFind, &data) < 0) \
             rv = -1; \
     } while (0)

-#define DO_TEST_FIND(name, vend, prod, bus, devno) \
-    DO_TEST_FIND_FULL(name, vend, prod, bus, devno, NULL, true, \
-                      FIND_BY_ALL, false)
-#define DO_TEST_FIND_FAIL(name, vend, prod, bus, devno) \
-    DO_TEST_FIND_FULL(name, vend, prod, bus, devno, NULL, true, \
-                      FIND_BY_ALL, true)
-
-#define DO_TEST_FIND_BY_BUS(name, bus, devno) \
-    DO_TEST_FIND_FULL(name, 101, 202, bus, devno, NULL, true, \
-                      FIND_BY_BUS, false)
-#define DO_TEST_FIND_BY_BUS_FAIL(name, bus, devno) \
-    DO_TEST_FIND_FULL(name, 101, 202, bus, devno, NULL, true, \
-                      FIND_BY_BUS, true)
-
 #define DO_TEST_FIND_BY_VENDOR(name, vend, prod) \
-    DO_TEST_FIND_FULL(name, vend, prod, 123, 456, NULL, true, \
+    DO_TEST_FIND_FULL(name, vend, prod, 123, 456, NULL, NULL, true, \
                       FIND_BY_VENDOR, false)
 #define DO_TEST_FIND_BY_VENDOR_FAIL(name, vend, prod) \
-    DO_TEST_FIND_FULL(name, vend, prod, 123, 456, NULL, true, \
+    DO_TEST_FIND_FULL(name, vend, prod, 123, 456, NULL, NULL, true, \
                       FIND_BY_VENDOR, true)

-    DO_TEST_FIND("Nexus", 0x18d1, 0x4e22, 1, 20);
-    DO_TEST_FIND_FAIL("Nexus wrong devnum", 0x18d1, 0x4e22, 1, 25);
-    DO_TEST_FIND_FAIL("Bogus", 0xf00d, 0xbeef, 1024, 768);
-
-    DO_TEST_FIND_BY_BUS("integrated camera", 1, 5);
-    DO_TEST_FIND_BY_BUS_FAIL("wrong bus/devno combination", 2, 20);
-    DO_TEST_FIND_BY_BUS_FAIL("missing bus", 5, 20);
-    DO_TEST_FIND_BY_BUS_FAIL("missing devnum", 1, 158);
+#define DO_TEST_FIND_BY_DEVICE(name, bus, devno) \
+    DO_TEST_FIND_FULL(name, 0x1010, 0x2020, bus, devno, NULL, NULL, true, \
+                      FIND_BY_DEVICE, false)
+#define DO_TEST_FIND_BY_DEVICE_FAIL(name, bus, devno) \
+    DO_TEST_FIND_FULL(name, 0x1010, 0x2020, bus, devno, NULL, NULL, true, \
+                      FIND_BY_DEVICE, true)
+
+#define DO_TEST_FIND_BY_PORT(name, bus, port) \
+    DO_TEST_FIND_FULL(name, 0x1010, 0x2020, bus, 456, port, NULL, true, \
+                      FIND_BY_PORT, false)
+#define DO_TEST_FIND_BY_PORT_FAIL(name, bus, port) \
+    DO_TEST_FIND_FULL(name, 0x1010, 0x2020, bus, 456, port, NULL, true, \
+                      FIND_BY_PORT, true)
+
+#define DO_TEST_FIND_BY_VENDOR_AND_DEVICE(name, vend, prod, bus, devno) \
+    DO_TEST_FIND_FULL(name, vend, prod, bus, devno, NULL, NULL, true, \
+                      FIND_BY_VENDOR_AND_DEVICE, false)
+#define DO_TEST_FIND_BY_VENDOR_AND_DEVICE_FAIL(name, vend, prod, bus, devno) \
+    DO_TEST_FIND_FULL(name, vend, prod, bus, devno, NULL, NULL, true, \
+                      FIND_BY_VENDOR_AND_DEVICE, true)
+
+#define DO_TEST_FIND_BY_VENDOR_AND_PORT(name, vend, prod, bus, port) \
+    DO_TEST_FIND_FULL(name, vend, prod, bus, 456, port, NULL, true, \
+                      FIND_BY_VENDOR_AND_PORT, false)
+#define DO_TEST_FIND_BY_VENDOR_AND_PORT_FAIL(name, vend, prod, bus, port) \
+    DO_TEST_FIND_FULL(name, vend, prod, bus, 456, port, NULL, true, \
+                      FIND_BY_VENDOR_AND_PORT, true)
+
+    DO_TEST_FIND_BY_DEVICE("integrated camera", 1, 5);
+    DO_TEST_FIND_BY_DEVICE_FAIL("wrong bus/devno combination", 2, 20);
+    DO_TEST_FIND_BY_DEVICE_FAIL("missing bus", 5, 20);
+    DO_TEST_FIND_BY_DEVICE_FAIL("missing devnum", 1, 158);

     DO_TEST_FIND_BY_VENDOR("Nexus (multiple results)", 0x18d1, 0x4e22);
     DO_TEST_FIND_BY_VENDOR_FAIL("Bogus vendor and product", 0xf00d, 0xbeef);
     DO_TEST_FIND_BY_VENDOR_FAIL("Valid vendor", 0x1d6b, 0xbeef);

+    DO_TEST_FIND_BY_PORT("Logitech mouse", 1, "1.5.3.3");
+    DO_TEST_FIND_BY_PORT_FAIL("wrong bus/port combination", 2, "1.5.3.3");
+    DO_TEST_FIND_BY_PORT_FAIL("missing bus", 5, "1.5.3.3");
+    DO_TEST_FIND_BY_PORT_FAIL("missing port", 1, "8.2.5");
+
+    DO_TEST_FIND_BY_VENDOR_AND_DEVICE("Nexus", 0x18d1, 0x4e22, 1, 20);
+    DO_TEST_FIND_BY_VENDOR_AND_DEVICE_FAIL("Bogus vendor and product", 0xf00d, 0xbeef, 1, 25);
+    DO_TEST_FIND_BY_VENDOR_AND_DEVICE_FAIL("Nexus wrong devnum", 0x18d1, 0x4e22, 1, 25);
+    DO_TEST_FIND_BY_VENDOR_AND_DEVICE_FAIL("Bogus", 0xf00d, 0xbeef, 1024, 768);
+
+    DO_TEST_FIND_BY_VENDOR_AND_PORT("Nexus", 0x046d, 0xc069, 1, "1.5.3.3");
+    DO_TEST_FIND_BY_VENDOR_AND_PORT_FAIL("Bogus vendor and product", 0xf00d, 0xbeef, 1, "1.5.3.3");
+    DO_TEST_FIND_BY_VENDOR_AND_PORT_FAIL("Nexus wrong port", 0x18d1, 0x4e22, 1, "8.2.5");
+    DO_TEST_FIND_BY_VENDOR_AND_PORT_FAIL("Bogus", 0xf00d, 0xbeef, 1024, "1.1.1.1");
+
     if (virTestRun("USB List test", testUSBList, NULL) < 0)
         rv = -1;

diff --git a/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.3.1/devpath b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.3.1/devpath
new file mode 100644
index 0000000000..02a7fbef02
--- /dev/null
+++ b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.3.1/devpath
@@ -0,0 +1 @@
+1.5.3.1
diff --git a/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.3.3/devpath b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.3.3/devpath
new file mode 100644
index 0000000000..23ca863cd4
--- /dev/null
+++ b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.3.3/devpath
@@ -0,0 +1 @@
+1.5.3.3
diff --git a/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.3/devpath b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.3/devpath
new file mode 100644
index 0000000000..8af85beb51
--- /dev/null
+++ b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.3/devpath
@@ -0,0 +1 @@
+1.5.3
diff --git a/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.4/devpath b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.4/devpath
new file mode 100644
index 0000000000..94fe62c274
--- /dev/null
+++ b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.4/devpath
@@ -0,0 +1 @@
+1.5.4
diff --git a/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.5/devpath b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.5/devpath
new file mode 100644
index 0000000000..9075be4951
--- /dev/null
+++ b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.5/devpath
@@ -0,0 +1 @@
+1.5.5
diff --git a/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.6/devpath b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.6/devpath
new file mode 100644
index 0000000000..eac1e0ada6
--- /dev/null
+++ b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.6/devpath
@@ -0,0 +1 @@
+1.5.6
diff --git a/tests/virusbtestdata/sys_bus_usb/devices/1-1.5/devpath b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5/devpath
new file mode 100644
index 0000000000..c239c60cba
--- /dev/null
+++ b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5/devpath
@@ -0,0 +1 @@
+1.5
diff --git a/tests/virusbtestdata/sys_bus_usb/devices/1-1.6/devpath b/tests/virusbtestdata/sys_bus_usb/devices/1-1.6/devpath
new file mode 100644
index 0000000000..810ee4e91e
--- /dev/null
+++ b/tests/virusbtestdata/sys_bus_usb/devices/1-1.6/devpath
@@ -0,0 +1 @@
+1.6
diff --git a/tests/virusbtestdata/sys_bus_usb/devices/1-1/devpath b/tests/virusbtestdata/sys_bus_usb/devices/1-1/devpath
new file mode 100644
index 0000000000..d00491fd7e
--- /dev/null
+++ b/tests/virusbtestdata/sys_bus_usb/devices/1-1/devpath
@@ -0,0 +1 @@
+1
diff --git a/tests/virusbtestdata/sys_bus_usb/devices/2-1.2/devpath b/tests/virusbtestdata/sys_bus_usb/devices/2-1.2/devpath
new file mode 100644
index 0000000000..5625e59da8
--- /dev/null
+++ b/tests/virusbtestdata/sys_bus_usb/devices/2-1.2/devpath
@@ -0,0 +1 @@
+1.2
diff --git a/tests/virusbtestdata/sys_bus_usb/devices/2-1/devpath b/tests/virusbtestdata/sys_bus_usb/devices/2-1/devpath
new file mode 100644
index 0000000000..d00491fd7e
--- /dev/null
+++ b/tests/virusbtestdata/sys_bus_usb/devices/2-1/devpath
@@ -0,0 +1 @@
+1
diff --git a/tests/virusbtestdata/sys_bus_usb/devices/usb1/devpath b/tests/virusbtestdata/sys_bus_usb/devices/usb1/devpath
new file mode 100644
index 0000000000..573541ac97
--- /dev/null
+++ b/tests/virusbtestdata/sys_bus_usb/devices/usb1/devpath
@@ -0,0 +1 @@
+0
diff --git a/tests/virusbtestdata/sys_bus_usb/devices/usb2/devpath b/tests/virusbtestdata/sys_bus_usb/devices/usb2/devpath
new file mode 100644
index 0000000000..573541ac97
--- /dev/null
+++ b/tests/virusbtestdata/sys_bus_usb/devices/usb2/devpath
@@ -0,0 +1 @@
+0
diff --git a/tests/virusbtestdata/sys_bus_usb/devices/usb3/devpath b/tests/virusbtestdata/sys_bus_usb/devices/usb3/devpath
new file mode 100644
index 0000000000..573541ac97
--- /dev/null
+++ b/tests/virusbtestdata/sys_bus_usb/devices/usb3/devpath
@@ -0,0 +1 @@
+0
diff --git a/tests/virusbtestdata/sys_bus_usb/devices/usb4/devpath b/tests/virusbtestdata/sys_bus_usb/devices/usb4/devpath
new file mode 100644
index 0000000000..573541ac97
--- /dev/null
+++ b/tests/virusbtestdata/sys_bus_usb/devices/usb4/devpath
@@ -0,0 +1 @@
+0
--
2.39.5
Re: [PATCH] USB hostdev: allow addressing by port
Posted by Michal Prívozník via Devel 1 year ago
On 3/17/25 21:59, Maximilian Martin via Devel wrote:
> From: Maximilian Martin <maximilian_martin@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_martin@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
Re: [PATCH] USB hostdev: allow addressing by port
Posted by Maximilian Martin via Devel 1 year ago
Ping :)

It's a much requested feature and certainly a low-hanging fruit.
I'm happy to integrate feedback!


Am 17.03.2025 um 21:59 schrieb Maximilian Martin via Devel:
> From: Maximilian Martin <maximilian_martin@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_martin@gmx.de>
> ---
>   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;
>       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)
> +        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)");
>           return -1;
>       }
>
> -    count = list->count;
>       if (devices)
>           *devices = list;
>       else
> @@ -224,86 +257,6 @@ virUSBDeviceFindByVendor(unsigned int vendor,
>       return count;
>   }
>
> -int
> -virUSBDeviceFindByBus(unsigned int bus,
> -                      unsigned int devno,
> -                      const char *vroot,
> -                      bool mandatory,
> -                      virUSBDevice **usb)
> -{
> -    virUSBDeviceList *list;
> -
> -    if (!(list = virUSBDeviceSearch(0, 0, bus, devno,
> -                                    vroot,
> -                                    USB_DEVICE_FIND_BY_BUS)))
> -        return -1;
> -
> -    if (list->count == 0) {
> -        virObjectUnref(list);
> -        if (!mandatory) {
> -            VIR_DEBUG("Did not find USB device bus:%u device:%u",
> -                      bus, devno);
> -            if (usb)
> -                *usb = NULL;
> -            return 0;
> -        }
> -
> -        virReportError(VIR_ERR_INTERNAL_ERROR,
> -                       _("Did not find USB device bus:%1$u device:%2$u"),
> -                       bus, devno);
> -        return -1;
> -    }
> -
> -    if (usb) {
> -        *usb = virUSBDeviceListGet(list, 0);
> -        virUSBDeviceListSteal(list, *usb);
> -    }
> -    virObjectUnref(list);
> -
> -    return 0;
> -}
> -
> -int
> -virUSBDeviceFind(unsigned int vendor,
> -                 unsigned int product,
> -                 unsigned int bus,
> -                 unsigned int devno,
> -                 const char *vroot,
> -                 bool mandatory,
> -                 virUSBDevice **usb)
> -{
> -    virUSBDeviceList *list;
> -
> -    unsigned int flags = USB_DEVICE_FIND_BY_VENDOR|USB_DEVICE_FIND_BY_BUS;
> -    if (!(list = virUSBDeviceSearch(vendor, product, bus, devno,
> -                                    vroot, flags)))
> -        return -1;
> -
> -    if (list->count == 0) {
> -        virObjectUnref(list);
> -        if (!mandatory) {
> -            VIR_DEBUG("Did not find USB device %04x:%04x bus:%u device:%u",
> -                      vendor, product, bus, devno);
> -            if (usb)
> -                *usb = NULL;
> -            return 0;
> -        }
> -
> -        virReportError(VIR_ERR_INTERNAL_ERROR,
> -                       _("Did not find USB device %1$04x:%2$04x bus:%3$u device:%4$u"),
> -                       vendor, product, bus, devno);
> -        return -1;
> -    }
> -
> -    if (usb) {
> -        *usb = virUSBDeviceListGet(list, 0);
> -        virUSBDeviceListSteal(list, *usb);
> -    }
> -    virObjectUnref(list);
> -
> -    return 0;
> -}
> -
>   virUSBDevice *
>   virUSBDeviceNew(unsigned int bus,
>                   unsigned int devno,
> diff --git a/src/util/virusb.h b/src/util/virusb.h
> index d2b3f69942..cfd1ad51cc 100644
> --- a/src/util/virusb.h
> +++ b/src/util/virusb.h
> @@ -30,30 +30,26 @@ typedef struct _virUSBDeviceList virUSBDeviceList;
>
>   G_DEFINE_AUTOPTR_CLEANUP_FUNC(virUSBDeviceList, virObjectUnref);
>
> +typedef enum {
> +    USB_DEVICE_ALL = 0,
> +    USB_DEVICE_FIND_BY_VENDOR = 1 << 0,
> +    USB_DEVICE_FIND_BY_DEVICE = 1 << 1,
> +    USB_DEVICE_FIND_BY_PORT = 1 << 2,
> +} virUSBDeviceFindFlags;
>
>   virUSBDevice *virUSBDeviceNew(unsigned int bus,
>                                   unsigned int devno,
>                                   const char *vroot);
>
> -int virUSBDeviceFindByBus(unsigned int bus,
> -                          unsigned int devno,
> -                          const char *vroot,
> -                          bool mandatory,
> -                          virUSBDevice **usb);
> -
> -int virUSBDeviceFindByVendor(unsigned int vendor,
> -                             unsigned int product,
> -                             const char *vroot,
> -                             bool mandatory,
> -                             virUSBDeviceList **devices);
> -
>   int virUSBDeviceFind(unsigned int vendor,
> -                     unsigned int product,
> -                     unsigned int bus,
> -                     unsigned int devno,
> -                     const char *vroot,
> -                     bool mandatory,
> -                     virUSBDevice **usb);
> +                 unsigned int product,
> +                 unsigned int bus,
> +                 unsigned int devno,
> +                 const char *port,
> +                 const char *vroot,
> +                 bool mandatory,
> +                 unsigned int flags,
> +                 virUSBDeviceList **devices);
>
>   void virUSBDeviceFree(virUSBDevice *dev);
>   int virUSBDeviceSetUsedBy(virUSBDevice *dev,
> diff --git a/tests/virusbtest.c b/tests/virusbtest.c
> index 870e136321..62b7b8bc29 100644
> --- a/tests/virusbtest.c
> +++ b/tests/virusbtest.c
> @@ -26,9 +26,11 @@
>   #define VIR_FROM_THIS VIR_FROM_NONE
>
>   typedef enum {
> -    FIND_BY_ALL,
>       FIND_BY_VENDOR,
> -    FIND_BY_BUS
> +    FIND_BY_DEVICE,
> +    FIND_BY_PORT,
> +    FIND_BY_VENDOR_AND_DEVICE,
> +    FIND_BY_VENDOR_AND_PORT
>   } testUSBFindFlags;
>
>   struct findTestInfo {
> @@ -37,6 +39,7 @@ struct findTestInfo {
>       unsigned int product;
>       unsigned int bus;
>       unsigned int devno;
> +    const char *port;
>       const char *vroot;
>       bool mandatory;
>       int how;
> @@ -70,25 +73,34 @@ static int testDeviceFind(const void *opaque)
>       g_autoptr(virUSBDeviceList) devs = NULL;
>       int rv = 0;
>       size_t i, ndevs = 0;
> +    unsigned int flags = 0;
>
>       switch (info->how) {
> -    case FIND_BY_ALL:
> -        rv = virUSBDeviceFind(info->vendor, info->product,
> -                              info->bus, info->devno,
> -                              info->vroot, info->mandatory, &dev);
> -        break;
>       case FIND_BY_VENDOR:
> -        rv = virUSBDeviceFindByVendor(info->vendor, info->product,
> -                                      info->vroot, info->mandatory, &devs);
> +        flags = USB_DEVICE_FIND_BY_VENDOR;
> +        break;
> +    case FIND_BY_DEVICE:
> +        flags = USB_DEVICE_FIND_BY_DEVICE;
> +        break;
> +    case FIND_BY_PORT:
> +        flags = USB_DEVICE_FIND_BY_PORT;
>           break;
> -    case FIND_BY_BUS:
> -        rv = virUSBDeviceFindByBus(info->bus, info->devno,
> -                                   info->vroot, info->mandatory, &dev);
> +    case FIND_BY_VENDOR_AND_DEVICE:
> +        flags = USB_DEVICE_FIND_BY_VENDOR |
> +                USB_DEVICE_FIND_BY_DEVICE;
> +        break;
> +    case FIND_BY_VENDOR_AND_PORT:
> +        flags = USB_DEVICE_FIND_BY_VENDOR |
> +                USB_DEVICE_FIND_BY_PORT;
>           break;
>       }
>
> +    rv = virUSBDeviceFind(info->vendor, info->product,
> +        info->bus, info->devno, info->port,
> +        info->vroot, info->mandatory, flags, &devs);
> +
>       if (info->expectFailure) {
> -        if (rv == 0) {
> +        if (rv >= 0) {
>               virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
>                              "unexpected success");
>           } else {
> @@ -99,9 +111,20 @@ static int testDeviceFind(const void *opaque)
>           goto cleanup;
>       }
>
> +    if (info->how != FIND_BY_VENDOR) {
> +        if (rv == 1) {
> +            dev = virUSBDeviceListGet(devs, 0);
> +            virUSBDeviceListSteal(devs, dev);
> +        } else {
> +            goto cleanup;
> +        }
> +    }
> +
>       switch (info->how) {
> -    case FIND_BY_ALL:
> -    case FIND_BY_BUS:
> +    case FIND_BY_DEVICE:
> +    case FIND_BY_PORT:
> +    case FIND_BY_VENDOR_AND_DEVICE:
> +    case FIND_BY_VENDOR_AND_PORT:
>           if (virUSBDeviceFileIterate(dev, testDeviceFileActor, NULL) < 0)
>               goto cleanup;
>           break;
> @@ -146,14 +169,17 @@ testUSBList(const void *opaque G_GNUC_UNUSED)
>       virUSBDeviceList *list = NULL;
>       virUSBDeviceList *devlist = NULL;
>       virUSBDevice *dev = NULL;
> +    virUSBDeviceList *devs = NULL;
>       int ret = -1;
> +    int rv;
>       size_t i, ndevs;
>
>       if (!(list = virUSBDeviceListNew()))
>           goto cleanup;
>
>   #define EXPECTED_NDEVS_ONE 3
> -    if (virUSBDeviceFindByVendor(0x1d6b, 0x0002, NULL, true, &devlist) < 0)
> +    if (virUSBDeviceFind(0x1d6b, 0x0002, 0, 0, NULL, NULL, true,
> +                         USB_DEVICE_FIND_BY_VENDOR, &devlist) < 0)
>           goto cleanup;
>
>       ndevs = virUSBDeviceListCount(devlist);
> @@ -176,7 +202,8 @@ testUSBList(const void *opaque G_GNUC_UNUSED)
>           goto cleanup;
>
>   #define EXPECTED_NDEVS_TWO 3
> -    if (virUSBDeviceFindByVendor(0x18d1, 0x4e22, NULL, true, &devlist) < 0)
> +    if (virUSBDeviceFind(0x18d1, 0x4e22, 0, 0, NULL, NULL, true,
> +                         USB_DEVICE_FIND_BY_VENDOR, &devlist) < 0)
>           goto cleanup;
>
>       ndevs = virUSBDeviceListCount(devlist);
> @@ -196,8 +223,16 @@ testUSBList(const void *opaque G_GNUC_UNUSED)
>                          EXPECTED_NDEVS_ONE + EXPECTED_NDEVS_TWO) < 0)
>           goto cleanup;
>
> -    if (virUSBDeviceFind(0x18d1, 0x4e22, 1, 20, NULL, true, &dev) < 0)
> +    rv = virUSBDeviceFind(0x18d1, 0x4e22, 1, 20, NULL, NULL, true,
> +                            USB_DEVICE_FIND_BY_VENDOR |
> +                            USB_DEVICE_FIND_BY_DEVICE, &devs);
> +    if (rv != 1) {
>           goto cleanup;
> +    } else {
> +        dev = virUSBDeviceListGet(devs, 0);
> +        virUSBDeviceListSteal(devs, dev);
> +    }
> +    virObjectUnref(devs);
>
>       if (!virUSBDeviceListFind(list, dev)) {
>           virReportError(VIR_ERR_INTERNAL_ERROR,
> @@ -229,49 +264,75 @@ mymain(void)
>   {
>       int rv = 0;
>
> -#define DO_TEST_FIND_FULL(name, vend, prod, bus, devno, vroot, mand, how, fail) \
> +#define DO_TEST_FIND_FULL(name, vend, prod, bus, devno, \
> +                          port, vroot, mand, how, fail) \
>       do { \
>           struct findTestInfo data = { name, vend, prod, bus, \
> -            devno, vroot, mand, how, fail \
> +            devno, port, vroot, mand, how, fail \
>           }; \
>           if (virTestRun("USBDeviceFind " name, testDeviceFind, &data) < 0) \
>               rv = -1; \
>       } while (0)
>
> -#define DO_TEST_FIND(name, vend, prod, bus, devno) \
> -    DO_TEST_FIND_FULL(name, vend, prod, bus, devno, NULL, true, \
> -                      FIND_BY_ALL, false)
> -#define DO_TEST_FIND_FAIL(name, vend, prod, bus, devno) \
> -    DO_TEST_FIND_FULL(name, vend, prod, bus, devno, NULL, true, \
> -                      FIND_BY_ALL, true)
> -
> -#define DO_TEST_FIND_BY_BUS(name, bus, devno) \
> -    DO_TEST_FIND_FULL(name, 101, 202, bus, devno, NULL, true, \
> -                      FIND_BY_BUS, false)
> -#define DO_TEST_FIND_BY_BUS_FAIL(name, bus, devno) \
> -    DO_TEST_FIND_FULL(name, 101, 202, bus, devno, NULL, true, \
> -                      FIND_BY_BUS, true)
> -
>   #define DO_TEST_FIND_BY_VENDOR(name, vend, prod) \
> -    DO_TEST_FIND_FULL(name, vend, prod, 123, 456, NULL, true, \
> +    DO_TEST_FIND_FULL(name, vend, prod, 123, 456, NULL, NULL, true, \
>                         FIND_BY_VENDOR, false)
>   #define DO_TEST_FIND_BY_VENDOR_FAIL(name, vend, prod) \
> -    DO_TEST_FIND_FULL(name, vend, prod, 123, 456, NULL, true, \
> +    DO_TEST_FIND_FULL(name, vend, prod, 123, 456, NULL, NULL, true, \
>                         FIND_BY_VENDOR, true)
>
> -    DO_TEST_FIND("Nexus", 0x18d1, 0x4e22, 1, 20);
> -    DO_TEST_FIND_FAIL("Nexus wrong devnum", 0x18d1, 0x4e22, 1, 25);
> -    DO_TEST_FIND_FAIL("Bogus", 0xf00d, 0xbeef, 1024, 768);
> -
> -    DO_TEST_FIND_BY_BUS("integrated camera", 1, 5);
> -    DO_TEST_FIND_BY_BUS_FAIL("wrong bus/devno combination", 2, 20);
> -    DO_TEST_FIND_BY_BUS_FAIL("missing bus", 5, 20);
> -    DO_TEST_FIND_BY_BUS_FAIL("missing devnum", 1, 158);
> +#define DO_TEST_FIND_BY_DEVICE(name, bus, devno) \
> +    DO_TEST_FIND_FULL(name, 0x1010, 0x2020, bus, devno, NULL, NULL, true, \
> +                      FIND_BY_DEVICE, false)
> +#define DO_TEST_FIND_BY_DEVICE_FAIL(name, bus, devno) \
> +    DO_TEST_FIND_FULL(name, 0x1010, 0x2020, bus, devno, NULL, NULL, true, \
> +                      FIND_BY_DEVICE, true)
> +
> +#define DO_TEST_FIND_BY_PORT(name, bus, port) \
> +    DO_TEST_FIND_FULL(name, 0x1010, 0x2020, bus, 456, port, NULL, true, \
> +                      FIND_BY_PORT, false)
> +#define DO_TEST_FIND_BY_PORT_FAIL(name, bus, port) \
> +    DO_TEST_FIND_FULL(name, 0x1010, 0x2020, bus, 456, port, NULL, true, \
> +                      FIND_BY_PORT, true)
> +
> +#define DO_TEST_FIND_BY_VENDOR_AND_DEVICE(name, vend, prod, bus, devno) \
> +    DO_TEST_FIND_FULL(name, vend, prod, bus, devno, NULL, NULL, true, \
> +                      FIND_BY_VENDOR_AND_DEVICE, false)
> +#define DO_TEST_FIND_BY_VENDOR_AND_DEVICE_FAIL(name, vend, prod, bus, devno) \
> +    DO_TEST_FIND_FULL(name, vend, prod, bus, devno, NULL, NULL, true, \
> +                      FIND_BY_VENDOR_AND_DEVICE, true)
> +
> +#define DO_TEST_FIND_BY_VENDOR_AND_PORT(name, vend, prod, bus, port) \
> +    DO_TEST_FIND_FULL(name, vend, prod, bus, 456, port, NULL, true, \
> +                      FIND_BY_VENDOR_AND_PORT, false)
> +#define DO_TEST_FIND_BY_VENDOR_AND_PORT_FAIL(name, vend, prod, bus, port) \
> +    DO_TEST_FIND_FULL(name, vend, prod, bus, 456, port, NULL, true, \
> +                      FIND_BY_VENDOR_AND_PORT, true)
> +
> +    DO_TEST_FIND_BY_DEVICE("integrated camera", 1, 5);
> +    DO_TEST_FIND_BY_DEVICE_FAIL("wrong bus/devno combination", 2, 20);
> +    DO_TEST_FIND_BY_DEVICE_FAIL("missing bus", 5, 20);
> +    DO_TEST_FIND_BY_DEVICE_FAIL("missing devnum", 1, 158);
>
>       DO_TEST_FIND_BY_VENDOR("Nexus (multiple results)", 0x18d1, 0x4e22);
>       DO_TEST_FIND_BY_VENDOR_FAIL("Bogus vendor and product", 0xf00d, 0xbeef);
>       DO_TEST_FIND_BY_VENDOR_FAIL("Valid vendor", 0x1d6b, 0xbeef);
>
> +    DO_TEST_FIND_BY_PORT("Logitech mouse", 1, "1.5.3.3");
> +    DO_TEST_FIND_BY_PORT_FAIL("wrong bus/port combination", 2, "1.5.3.3");
> +    DO_TEST_FIND_BY_PORT_FAIL("missing bus", 5, "1.5.3.3");
> +    DO_TEST_FIND_BY_PORT_FAIL("missing port", 1, "8.2.5");
> +
> +    DO_TEST_FIND_BY_VENDOR_AND_DEVICE("Nexus", 0x18d1, 0x4e22, 1, 20);
> +    DO_TEST_FIND_BY_VENDOR_AND_DEVICE_FAIL("Bogus vendor and product", 0xf00d, 0xbeef, 1, 25);
> +    DO_TEST_FIND_BY_VENDOR_AND_DEVICE_FAIL("Nexus wrong devnum", 0x18d1, 0x4e22, 1, 25);
> +    DO_TEST_FIND_BY_VENDOR_AND_DEVICE_FAIL("Bogus", 0xf00d, 0xbeef, 1024, 768);
> +
> +    DO_TEST_FIND_BY_VENDOR_AND_PORT("Nexus", 0x046d, 0xc069, 1, "1.5.3.3");
> +    DO_TEST_FIND_BY_VENDOR_AND_PORT_FAIL("Bogus vendor and product", 0xf00d, 0xbeef, 1, "1.5.3.3");
> +    DO_TEST_FIND_BY_VENDOR_AND_PORT_FAIL("Nexus wrong port", 0x18d1, 0x4e22, 1, "8.2.5");
> +    DO_TEST_FIND_BY_VENDOR_AND_PORT_FAIL("Bogus", 0xf00d, 0xbeef, 1024, "1.1.1.1");
> +
>       if (virTestRun("USB List test", testUSBList, NULL) < 0)
>           rv = -1;
>
> diff --git a/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.3.1/devpath b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.3.1/devpath
> new file mode 100644
> index 0000000000..02a7fbef02
> --- /dev/null
> +++ b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.3.1/devpath
> @@ -0,0 +1 @@
> +1.5.3.1
> diff --git a/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.3.3/devpath b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.3.3/devpath
> new file mode 100644
> index 0000000000..23ca863cd4
> --- /dev/null
> +++ b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.3.3/devpath
> @@ -0,0 +1 @@
> +1.5.3.3
> diff --git a/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.3/devpath b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.3/devpath
> new file mode 100644
> index 0000000000..8af85beb51
> --- /dev/null
> +++ b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.3/devpath
> @@ -0,0 +1 @@
> +1.5.3
> diff --git a/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.4/devpath b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.4/devpath
> new file mode 100644
> index 0000000000..94fe62c274
> --- /dev/null
> +++ b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.4/devpath
> @@ -0,0 +1 @@
> +1.5.4
> diff --git a/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.5/devpath b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.5/devpath
> new file mode 100644
> index 0000000000..9075be4951
> --- /dev/null
> +++ b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.5/devpath
> @@ -0,0 +1 @@
> +1.5.5
> diff --git a/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.6/devpath b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.6/devpath
> new file mode 100644
> index 0000000000..eac1e0ada6
> --- /dev/null
> +++ b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5.6/devpath
> @@ -0,0 +1 @@
> +1.5.6
> diff --git a/tests/virusbtestdata/sys_bus_usb/devices/1-1.5/devpath b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5/devpath
> new file mode 100644
> index 0000000000..c239c60cba
> --- /dev/null
> +++ b/tests/virusbtestdata/sys_bus_usb/devices/1-1.5/devpath
> @@ -0,0 +1 @@
> +1.5
> diff --git a/tests/virusbtestdata/sys_bus_usb/devices/1-1.6/devpath b/tests/virusbtestdata/sys_bus_usb/devices/1-1.6/devpath
> new file mode 100644
> index 0000000000..810ee4e91e
> --- /dev/null
> +++ b/tests/virusbtestdata/sys_bus_usb/devices/1-1.6/devpath
> @@ -0,0 +1 @@
> +1.6
> diff --git a/tests/virusbtestdata/sys_bus_usb/devices/1-1/devpath b/tests/virusbtestdata/sys_bus_usb/devices/1-1/devpath
> new file mode 100644
> index 0000000000..d00491fd7e
> --- /dev/null
> +++ b/tests/virusbtestdata/sys_bus_usb/devices/1-1/devpath
> @@ -0,0 +1 @@
> +1
> diff --git a/tests/virusbtestdata/sys_bus_usb/devices/2-1.2/devpath b/tests/virusbtestdata/sys_bus_usb/devices/2-1.2/devpath
> new file mode 100644
> index 0000000000..5625e59da8
> --- /dev/null
> +++ b/tests/virusbtestdata/sys_bus_usb/devices/2-1.2/devpath
> @@ -0,0 +1 @@
> +1.2
> diff --git a/tests/virusbtestdata/sys_bus_usb/devices/2-1/devpath b/tests/virusbtestdata/sys_bus_usb/devices/2-1/devpath
> new file mode 100644
> index 0000000000..d00491fd7e
> --- /dev/null
> +++ b/tests/virusbtestdata/sys_bus_usb/devices/2-1/devpath
> @@ -0,0 +1 @@
> +1
> diff --git a/tests/virusbtestdata/sys_bus_usb/devices/usb1/devpath b/tests/virusbtestdata/sys_bus_usb/devices/usb1/devpath
> new file mode 100644
> index 0000000000..573541ac97
> --- /dev/null
> +++ b/tests/virusbtestdata/sys_bus_usb/devices/usb1/devpath
> @@ -0,0 +1 @@
> +0
> diff --git a/tests/virusbtestdata/sys_bus_usb/devices/usb2/devpath b/tests/virusbtestdata/sys_bus_usb/devices/usb2/devpath
> new file mode 100644
> index 0000000000..573541ac97
> --- /dev/null
> +++ b/tests/virusbtestdata/sys_bus_usb/devices/usb2/devpath
> @@ -0,0 +1 @@
> +0
> diff --git a/tests/virusbtestdata/sys_bus_usb/devices/usb3/devpath b/tests/virusbtestdata/sys_bus_usb/devices/usb3/devpath
> new file mode 100644
> index 0000000000..573541ac97
> --- /dev/null
> +++ b/tests/virusbtestdata/sys_bus_usb/devices/usb3/devpath
> @@ -0,0 +1 @@
> +0
> diff --git a/tests/virusbtestdata/sys_bus_usb/devices/usb4/devpath b/tests/virusbtestdata/sys_bus_usb/devices/usb4/devpath
> new file mode 100644
> index 0000000000..573541ac97
> --- /dev/null
> +++ b/tests/virusbtestdata/sys_bus_usb/devices/usb4/devpath
> @@ -0,0 +1 @@
> +0
> --
> 2.39.5