Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions doc/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4402,6 +4402,77 @@ to achieve the same effect:
match:
'@ID_PATH': 'pci-0000:05:00.0-usb-3-1.4'

.. _exporter-hub-abstraction:

USB Hub Abstraction
~~~~~~~~~~~~~~~~~~~
When a lab uses USB hubs with many ports, the raw ``ID_PATH`` strings in
match entries become long and hard to maintain. The exporter supports a
``hubs`` section that defines USB hubs by name, with a base path and a
mapping from logical port numbers to USB path suffixes. Resources can
then use ``hub`` and ``port`` in their match dict instead of a raw
``ID_PATH``.

Define hubs at the top level of the exporter configuration:

.. code-block:: yaml

hubs:
a:
base: 'pci-0000:00:14.0-usb-0:10'
ports:
1: '2.1'
2: '2.2'
3: '2.3'
# ...
b:
base: 'pci-0000:04:00.0-usb-0:2'
ports:
1: '1.1'
2: '1.2'
# ...

Each hub has a ``base`` path (the PCI path up to the hub's root port) and
a ``ports`` mapping from logical port number to the USB path suffix for
that port. The port suffixes depend on the hub's internal topology and
must be determined for each hub model (for example by plugging a device
into each port and checking ``udevadm info``).

Resources reference a hub port using ``hub`` and ``port`` in their match
dict. For resources that need an ancestor match (like serial ports),
add ``iface`` to specify the USB interface number:

.. code-block:: yaml

board1:
USBSerialPort:
match:
hub: a
port: 3
iface: '1.0'

HIDRelay:
index: 4
match:
hub: a
port: 14

When ``iface`` is present, the expansion produces an ``@ID_PATH`` (ancestor
match) with the interface appended after a colon:
``@ID_PATH: pci-0000:00:14.0-usb-0:10.2.3:1.0``.

When ``iface`` is absent, the expansion produces a plain ``ID_PATH`` (direct
match) with no interface suffix:
``ID_PATH: pci-0000:00:14.0-usb-0:10.1.2``.

Other match keys (such as ``ID_SERIAL_SHORT``) can be used alongside
``hub``/``port`` and are preserved in the expanded match. Resources that
do not use hub/port matching (e.g. those matched by serial number) are
unaffected.

The ``hubs`` section is removed from the configuration data after
expansion and does not appear as a resource group.

Templating the Exporter Configuration
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To reduce the amount of repeated declarations when many similar resources
Expand Down
46 changes: 46 additions & 0 deletions doc/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -839,3 +839,49 @@ like this:
$ labgrid-client -p example allow sirius/john

To remove the allow it is currently necessary to unlock and lock the place.

Simplifying USB device matching with hubs
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Labs with many USB devices often use multi-port USB hubs, and the raw
``ID_PATH`` strings needed to match each device can be long and error-prone.
The exporter supports a ``hubs`` section in the configuration file that maps
logical hub names and port numbers to USB paths, so that resources can be
described more concisely.

For example, instead of writing::

board1:
USBSerialPort:
match:
'@ID_PATH': 'pci-0000:00:14.0-usb-0:10.2.3:1.0'

you can define the hub once and reference it by name::

hubs:
a:
base: 'pci-0000:00:14.0-usb-0:10'
ports:
1: '2.1'
2: '2.2'
3: '2.3'

board1:
USBSerialPort:
match:
hub: a
port: 3
iface: '1.0'

The ``iface`` field controls both the USB interface suffix and the match
type: when present, the result is an ``@ID_PATH`` ancestor match with the
interface appended (for serial ports and similar multi-interface devices);
when absent, the result is a plain ``ID_PATH`` direct match (for relays,
USB loaders, etc.).

To determine the port mapping for a hub, plug a device into each port and
check its path with ``udevadm info``. The ``base`` is the common prefix
and each port's suffix is the remainder.

See :ref:`USB Hub Abstraction <exporter-hub-abstraction>` in the
configuration reference for full details.
77 changes: 77 additions & 0 deletions labgrid/remote/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,82 @@
reexec = False


def _expand_hubs(data):
"""Expand hub/port references in match dicts to ID_PATH values.

If the config data contains a top-level 'hubs' key, it is popped and
used to resolve any match dicts that contain 'hub' and 'port' keys
into a full ID_PATH string.

When 'iface' is also present, the result uses '@ID_PATH' (ancestor
match) with the interface appended after a colon. Without 'iface',
the result uses 'ID_PATH' (direct match) with no interface suffix.

For example, given::

hubs:
a:
base: 'pci-0000:04:00.0-usb-0:2'
ports:
7: '2.3'

a match dict ``{'hub': 'a', 'port': 7, 'iface': '1.0'}`` becomes
``{'@ID_PATH': 'pci-0000:04:00.0-usb-0:2.2.3:1.0'}``.

Without iface, ``{'hub': 'a', 'port': 7}`` becomes
``{'ID_PATH': 'pci-0000:04:00.0-usb-0:2.2.3'}``.
"""
hubs = data.pop("hubs", None)
if not hubs:
return

for group_name, group in data.items():
if not isinstance(group, dict):
continue
for resource_name, params in group.items():
if not isinstance(params, dict):
continue
match = params.get("match")
if not isinstance(match, dict):
continue

hub_name = match.get("hub")
port_num = match.get("port")
if hub_name is None and port_num is None:
continue
if hub_name is None or port_num is None:
raise ExporterError(
f"{group_name}/{resource_name}: 'hub' and 'port' must both be specified in a match"
)

hub = hubs.get(hub_name)
if hub is None:
raise ExporterError(
f"{group_name}/{resource_name}: hub '{hub_name}' is not defined in the hubs section"
)

ports = hub.get("ports", {})
# YAML may parse port keys as integers or strings
suffix = ports.get(port_num)
if suffix is None:
suffix = ports.get(str(port_num))
if suffix is None:
suffix = ports.get(int(port_num))
if suffix is None:
raise ExporterError(
f"{group_name}/{resource_name}: port {port_num} is not defined in hub '{hub_name}'"
)

iface = match.get("iface")
del match["hub"]
del match["port"]
if iface is not None:
del match["iface"]
match["@ID_PATH"] = f"{hub['base']}.{suffix}:{iface}"
else:
match["ID_PATH"] = f"{hub['base']}.{suffix}"


class ExporterError(Exception):
pass

Expand Down Expand Up @@ -852,6 +928,7 @@ async def run(self) -> None:
"name": self.name,
}
resource_config = ResourceConfig(self.config["resources"], config_template_env)
_expand_hubs(resource_config.data)
for group_name, group in resource_config.data.items():
group_name = str(group_name)
for resource_name, params in group.items():
Expand Down
Loading
Loading