Skip to content

Add shared helpers for live hardware reports - #2879

Open
berendt wants to merge 8 commits into
mainfrom
implement/issue-2876-hardware-report-groundwork
Open

berendt wants to merge 8 commits into
mainfrom
implement/issue-2876-hardware-report-groundwork

Conversation

@berendt

@berendt berendt commented Nov 11, 2026 •

Copy link
Copy Markdown
Member

This pull request adds osism/utils/hardware/, the groundwork shared by osism report transceiver (#2876) and the cabling check in osism report lldp (#2877). It adds no command and changes no existing one.

It is the first of a stack of three. The two commands follow in their own pull requests on top of this branch.

Commits

  1. Add run_remote helper for non-interactive SSH commands: osism/utils/ssh.py gets run_remote(), the ssh call the report commands already build, with BatchMode=yes, stdin and a timeout.
  2. Let the SONiC interface cache take interfaces fetched in bulk: InterfaceCache.set_device_interfaces(), so that convert_netbox_interface_to_sonic() can work on interfaces loaded with one query instead of fetching them per switch.
  3. Add the hardware report package with MAC and sort helpers: normalize_mac() and natural_key().
  4. Add device selection for the hardware reports: NetBox filters, name patterns, --type, --limit, classification into nodes and SONiC switches, the handling of switches outside NETBOX_FILTER_CONDUCTOR_SONIC, the inventory fallback without NetBox, and address resolution.
  5. Add parallel SSH collection with Linux and SONiC collectors: one SSH session per device from a thread pool. Each session pipes a stdlib-only collector to python3 - and gets one JSON document of raw data back. dump_raw() writes those documents to files.
  6. Add parsers for collected port and LLDP data: port records for nodes and switches, and neighbour records from lldpctl -f json.
  7. Add matching of live ports to NetBox interfaces: bulk fetch of interfaces as plain data including the far end of the cable path, and the mapping methods port-config, alias, name for switches and mac, name, label, lldp for nodes.
  8. Add table, CSV and JSON rendering for hardware reports: --format, --column, --long, --output.

Verification

The unit tests of this revision run in CI; they were not run locally. Every commit of the stack was checked with black --check and flake8, and all test modules import at every commit. mypy --ignore-missing-imports reports nothing in the new modules.

The parsers were run against the complete captures before the fixtures were derived from them, and the expectations of the new tests were taken from those runs.

Checked against captures

The collectors ran on three nodes and two SONiC switches of a deployment (#2884): nodes with ConnectX-4 Lx, BCM57414, X710, 82599ES and I350 NICs on Ubuntu 24.04, and two Accton AS7726-32X running Enterprise SONiC 4.4.1 and 4.5.2.

  • Both collectors ran unchanged on these devices, piped to python3 - over SSH.
  • sonic-db-dump works as the login user on both SONiC releases. The login user is a member of the redis group there.
  • The port records match what the devices report: breakout ports with their alias (Eth1/1/1), lanes and index on the switches, altnames and permanent MAC addresses on the nodes.
  • The host interface of a BMC shows up on the nodes as a USB network device. It is a physical port without a PCI address.
  • A switch that OSISM configured announces the alias of a port as port ID and the SONiC name as description. lldpd on a node announces the MAC address of the port and its name. A RouterOS switch announces a bridge port as bridge/ether9; the matching compares it by the name of the port.

linux-connectx4.json and sonic-enterprise.json under tests/unit/utils/hardware/fixtures/ are captures, anonymized and reduced to a selection of ports. The README next to them lists the origin of every file.

Not verified

  • None of the captured nodes has a bond. The handling of bond members is tested against the synthetic fixture linux-mellanox.json only.
  • The retry of sonic-db-dump with sudo -n was not needed on the captured switches and has not run on a device.
  • Nothing in this pull request was run against NetBox.

Part of #2876 and #2877.

Assisted-by: Claude:claude-fable-5-2

The report commands each build the same ssh command line and call it
once per value they need. The hardware reports open one session per
device, pipe a collector script to the remote python3, and run many
sessions from worker threads, where a password prompt would block a
thread for good.

run_remote() runs the ssh binary with the options the report commands
already use, adds BatchMode=yes, passes text on stdin and bounds the
call with a timeout.

Tests: the command line, stdin and timeout handling, and that the
timeout exception reaches the caller.

Part of #2876 and #2877.

Assisted-by: Claude:claude-fable-5-2
Signed-off-by: Christian Berendt <berendt@osism.tech>
convert_netbox_interface_to_sonic() reads the interfaces of a device
from the thread-local interface cache and fetches them from NetBox on
a miss, one request per device. A caller that already loaded the
interfaces of many devices with a single query had no way to hand
them over.

set_device_interfaces() stores such a list, so that later lookups for
that device are served from the cache.

Tests: a seeded device is served without a NetBox request, the list
is copied, and a second call replaces the entry.

Part of #2876 and #2877.

Assisted-by: Claude:claude-fable-5-2
Signed-off-by: Christian Berendt <berendt@osism.tech>
osism.utils.hardware will hold what the live hardware reports share:
device selection, collection over SSH, parsing, interface mapping and
rendering.

It starts with two helpers used throughout. normalize_mac() brings
the spellings found in sysfs, ethtool, LLDP and NetBox into one form
and rejects the all-zero address. natural_key() sorts interface names
by the value of their numbers, Ethernet2 before Ethernet10.

Part of #2876 and #2877.

Assisted-by: Claude:claude-fable-5-2
Signed-off-by: Christian Berendt <berendt@osism.tech>
The existing report commands take their hosts from the Ansible
inventory, which knows the nodes but not the SONiC switches, and can
only be narrowed with an Ansible pattern.

select_targets() queries NetBox with the filters given on the command
line (site, role, rack, tag and so on, plus a generic KEY=VALUE
filter), classifies every device as node or switch, and applies
shell-style name patterns. A switch has to match
NETBOX_FILTER_CONDUCTOR_SONIC, because those are the switches the
operator key is deployed to; others are skipped with a warning unless
--include-unmanaged is given. A node is a device whose hostname is in
the inventory. Without NetBox the inventory is the only source and
NetBox filters are rejected.

resolve_addresses() then sets the SSH address and user. A switch is
reached at the management IP of its config context, its OOB IP or its
primary IP, so it does not need a synced config context.

Targets carry the NetBox device as plain data. The collection can
then run in threads without sharing the NetBox session.

Tests: query building, selection with and without NetBox, the
handling of unmanaged switches, address resolution, and the inventory
and NetBox failure paths.

Part of #2876 and #2877.

Assisted-by: Claude:claude-fable-5-2
Signed-off-by: Christian Berendt <berendt@osism.tech>
The hardware reports need many values per device from many devices.
One ssh call per value, device after device, as the existing report
commands do it, does not scale to that.

collect() opens one session per device from a thread pool. The
session pipes a stdlib-only collector to python3 on the device, and
the collector prints one JSON document of raw command output, sysfs
values or Redis dumps. A device that times out, refuses the login or
returns garbage is recorded as failed and does not hold up the rest.
dump_raw() writes the documents to files, which is how parser
fixtures are captured from real devices.

The collectors gather and do not parse. This first version covers
what both reports need: on Linux the physical ports from sysfs, link
state, the permanent MAC and the LLDP neighbours; on SONiC the PORT
tables of CONFIG_DB and APPL_DB, the device metadata and the
management port. sonic-db-dump is tried as the login user and again
with sudo, because the access to Redis differs between releases and
was not verified on a switch.

Tests: failure handling, parallelism and its limit, the collectors
against a fake sysfs tree and a fake command runner.

Part of #2876 and #2877.

Assisted-by: Claude:claude-fable-5-2
Signed-off-by: Christian Berendt <berendt@osism.tech>
The collectors return raw output. These parsers turn it into the
records the reports work with.

parse_linux and parse_sonic produce one port record per physical
port with name, alias, altnames, admin and oper state, speed, MTU and
MAC addresses. A bond member keeps its permanent MAC, since its
address is the MAC of the bond while NetBox documents the one burned
into the NIC. A port on a USB device, such as the host interface of
a BMC, has no PCI address. parse_lldp normalises lldpctl JSON into
neighbour records and keeps several neighbours per port.

The fixtures are collector documents in the format --dump-raw writes.
linux-connectx4.json and sonic-enterprise.json were captured from a
node and from a switch running Enterprise SONiC 4.5.2, anonymized
and reduced to a selection of ports. linux-mellanox.json and
sonic-synthetic.json are synthetic: a node with a bond, an optical
module and an empty cage, which were not captured, and the switch
it is cabled to. The README next to them says which is which.

Tests: the parsers against the fixtures and against malformed input.

Part of #2876 and #2877.

Assisted-by: Claude:claude-fable-5-2
Signed-off-by: Christian Berendt <berendt@osism.tech>
A port is called ens1f0np0 or Ethernet0 on the device and something
else in NetBox. Both reports have to bring the two together, and have
to show how they did it, so that a wrong match is visible.

fetch_interfaces() loads the interfaces of all devices with a few
chunked queries and converts the records to plain data at once,
including the far end of the cable path. Reading a missing attribute
from a pynetbox record would fetch that record again, once per
interface.

Switch ports are matched through the HWSKU port config, then by
alias, then by name. The translation is the one the config generator
uses; the interface cache is seeded from the bulk fetch so that it
does not query NetBox per switch. The management port is matched
against the mgmt_only interfaces. Node ports are matched by permanent
MAC, name, label and finally by their LLDP neighbour.

A neighbour is compared by the names it gives its port: the port ID
and the port description. RouterOS announces a port that is member
of a bridge as bridge/ether9; the name of the port alone is one of
its names as well.

Tests: conversion of API data including real pynetbox records, the
translation against the port config files of the repository, and
every mapping method with its precedence.

Part of #2876 and #2877.

Assisted-by: Claude:claude-fable-5-2
Signed-off-by: Christian Berendt <berendt@osism.tech>
The report commands print a psql table and nothing else. The hardware
reports are also meant to be read by scripts and spreadsheets.

render provides the --format, --column, --long and --output options
and renders flat rows as table or CSV with a selectable set of
columns. Table cells are not parsed as numbers, which would turn a
serial number such as 1E5 into 100000.0. JSON is rendered from a
document the command builds, because it is nested per device.

Tests: column selection, flattening, natural sorting, the three
formats, and that writing to a file leaves stdout empty.

Part of #2876 and #2877.

Assisted-by: Claude:claude-fable-5-2
Signed-off-by: Christian Berendt <berendt@osism.tech>
@berendt
berendt force-pushed the implement/issue-2876-hardware-report-groundwork branch from 2442c8e to 2e9df2f Compare November 13, 2026 13:08
@berendt
berendt marked this pull request as ready for review November 13, 2026 13:14
@sourcery-ai

sourcery-ai Bot commented Nov 13, 2026

Copy link
Copy Markdown

Sorry @berendt, your pull request is larger than the review limit of 150,000 diff characters

@berendt
berendt requested a review from ideaship November 13, 2026 13:14
@berendt berendt moved this from In progress to Ready for review in Human Board Nov 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Ready for review

Development

Successfully merging this pull request may close these issues.

1 participant