Repository navigation
Description
Category: feature | Scope: Large
Summary
Add a command osism report transceiver that collects, live over SSH, the
pluggable transceivers and the surrounding hardware of every physical network
port on OSISM-managed Linux nodes and SONiC switches, and prints the
result as one report (table, CSV or JSON).
Per physical port the report contains:
- the transceiver (presence, form factor, vendor, part number, serial, revision,
firmware where the module exposes one, optional DOM readings), - the port state (admin up/down, link up/down, speed, FEC, MTU, MAC),
- the hardware carrying the port: NIC vendor, model, driver and firmware on a
node; platform, HWSKU, ASIC and SONiC version on a switch, - the mapping between the interface name on the device and the interface name
in NetBox.
Devices are selectable by NetBox attributes (site, device role, rack, tag, ...)
and by name. The goal is a detailed, current hardware view of each device
without logging in to it.
Motivation
There is no way today to answer "which optics are plugged in where" across a
deployment. The information is needed for spare-part planning, RMA handling,
vendor/firmware compatibility checks before upgrades, and for debugging link
problems, where both ends of a cable have to be compared. Today that means
running ethtool -m on each node and show interfaces transceiver on each
switch by hand, and translating between the names used on the device and the
names used in NetBox.
Analysis of the current code base
Existing report commands
osism/commands/report.py already contains four live, SSH-based reports
(report memory, report lldp, report bgp, report status). They all follow
the same pattern, which the new command can build on but cannot reuse as is:
| Aspect | Today | Needed |
|---|---|---|
| Target selection | ansible-inventory --list [--limit] via get_inventory_path() / get_hosts_from_inventory() |
NetBox-driven selection, SONiC switches are not Ansible hosts |
| Connection | /usr/bin/ssh subprocess, settings.OPERATOR_USER, key /ansible/secrets/id_rsa.operator, resolve_host_with_fallback() (DNS, then NetBox primary_ip4) |
same for nodes; switches need the SONiC path below |
| Execution | serial loop, one or two SSH calls per host, 30 s timeout each | many values per host and many hosts: one SSH session per device, devices in parallel |
| Output | tabulate(..., tablefmt="psql") only |
table, CSV, JSON |
| Code | ~70 lines of inventory/SSH boilerplate duplicated in each of the four classes | shared helper |
No command in the repository produces CSV. --format {table,json} plus a
repeatable --column exists in osism/commands/redfish.py and is the closest
convention. Logging goes to stderr (osism/main.py), so stdout can carry
machine-readable output safely.
Existing SONiC SSH path
osism/commands/sonic.py, SonicCommandBase:
_get_device_from_netbox()finds the device byname, then by the custom
fieldinventory_hostname._get_ssh_connection_details()takesmanagement.ip/management.username
fromlocal_context_data, falls back toget_device_oob_ip()
(osism/tasks/conductor/netbox.py), and defaults the user toadmin._create_ssh_connection()uses paramiko with the operator key and
/share/known_hosts.sonic show <host> <cmd>proves that the login shell accepts SONiC CLI
commands directly.
_get_config_context() fails when a device has no local_context_data. The
report must not depend on that: a switch that was never synced still has
transceivers. It should resolve the address from the OOB IP or the primary IP
on its own.
Existing NetBox selection
osism/tasks/conductor/sonic/device.py:get_devices()selects SONiC switches
withNETBOX_FILTER_CONDUCTOR_SONIC(default
status=active, tag=managed-by-metalbox) and a role in
DEFAULT_SONIC_ROLES(osism/tasks/conductor/sonic/constants.py).get_nb_device_query_list_sonic()documents the supported filter keys
(site,region,site_group,location,rack,tag,status) and
the fact that NetBox only acceptslocation_id/rack_id, so names are
resolved to IDs first.utils.nbis a lazily initialised pynetbox client that CLI commands use
directly (console.py,sonic.py). It isNonewhen NetBox is not
configured, which is a supported setup for the node-only reports.
Existing interface name mapping
osism/tasks/conductor/sonic/interface.py already translates between NetBox
and SONiC names:
convert_netbox_interface_to_sonic(interface, device)mapsEth1/1,
Eth1/1/1(breakout),Eth1(Port1)andEthernetNto the SONiC name
EthernetN, using the HWSKU from thesonic_parameterscustom field and
get_port_config(hwsku)(/etc/sonic/port_config/<hwsku>.ini, installed
into the image by theContainerfile).convert_sonic_interface_to_alias()is the reverse direction.- On failure
convert_netbox_interface_to_sonic()returns the input name
unchanged, so a caller has to check the result against^Ethernet\d+$to
detect an unmapped interface.
Nothing comparable exists for Linux nodes. The only link the code relies on is
the MAC address: osism/tasks/conductor/ironic.py creates Ironic ports from
interface.mac_address of every enabled, non-management NetBox interface.
Runtime environment
The CLI image contains openssh-client, paramiko, pynetbox, tabulate and the
SONiC port_config files. Nothing has to be added to the image. On the target
side the nodes need python3 (present, Ansible requires it), ethtool,
iproute2, pciutils; devlink and lldpctl are used when available.
Proposed command
osism report transceiver [NAME ...]
[--type {all,node,switch}] [--include-unmanaged]
[--include-vf] [--include-infiniband]
[-l PATTERN | --limit PATTERN]
[--site SLUG] [--role SLUG] [--rack NAME] [--location NAME]
[--region SLUG] [--site-group SLUG] [--tenant SLUG] [--tag SLUG]
[--manufacturer SLUG] [--device-type SLUG] [--platform SLUG]
[--status STATUS] [--filter KEY=VALUE]
[--interface PATTERN] [--present] [--oper-status {up,down}]
[--format {table,csv,json}] [--column NAME] [--long] [--output FILE]
[--dom] [--parallel N] [--timeout SECONDS]
Examples:
# everything known, default columns
osism report transceiver
# all leaf switches of one site as CSV
osism report transceiver --site fra1 --role leaf --format csv --output optics.csv
# one node, full hardware view as JSON
osism report transceiver compute-042 --format json
# name patterns, only ports with a module plugged in
osism report transceiver 'leaf-1*' 'spine-*' --present
# all Ceph nodes via an Ansible pattern, with optical power readings
osism report transceiver -l ceph-osd --dom --long
Registration in setup.cfg: report transceiver = osism.commands.report:Transceiver.
Device selection
- NetBox available (the normal case): query
dcim.deviceswith
status=active(overridable with--status) plus the given filters.- Typed options map to NetBox device filters:
site,role,region,
site_group,tenant,tag,manufacturer,device_type,platform
take slugs;rackandlocationtake names and are resolved to IDs the
wayget_nb_device_query_list_sonic()does it. - Every option is repeatable. Values of one option are combined the way
NetBox combines a repeated query parameter, different options are ANDed. --filter KEY=VALUE(repeatable) passes any other NetBox device filter
through unchanged, for example--filter cf_maintenance=false.
- Typed options map to NetBox device filters:
- Classification of each device found:
switch: role slug inDEFAULT_SONIC_ROLESand matching
NETBOX_FILTER_CONDUCTOR_SONIC, i.e. the same setsonic syncand
sonic listoperate on. Those are the switches the operator key is
deployed to. A switch with a SONiC role that does not match
NETBOX_FILTER_CONDUCTOR_SONICis skipped, also when it is named
explicitly; the skip is logged as a warning in that case so it is not
silent.--include-unmanagedlifts the restriction: every selected
device with a role inDEFAULT_SONIC_ROLESis then queried.node: the device's hostname (get_device_hostname(): custom field
inventory_hostname, elsename) is a host in the Ansible inventory.- anything else (PDUs, unmanaged switches, firewalls, ...) is skipped and
listed at debug level.
- Name filter: positional
NAMEarguments are shell-style patterns
(fnmatch), matched against the NetBox device name and the inventory
hostname. A device is selected when any pattern matches. Matching happens
client-side, so it does not depend on the lookup expressions of a specific
NetBox version. --limit PATTERNis the Ansible host pattern known from the other report
commands (groups, ranges, exclusions). It is evaluated against the
inventory and therefore only ever selects nodes.--typerestricts the result to nodes or switches.- NetBox not configured (
utils.nbisNone): the command still works
for nodes from the inventory, with--limitandNAME. NetBox filters are
rejected with a clear error, switches cannot be discovered, and the NetBox
interface column stays empty.
Collection
One SSH session per device; devices are processed in parallel with a
ThreadPoolExecutor (--parallel, default 10). --timeout (default 60 s)
bounds one device. All NetBox calls happen in the main thread before and after
the SSH fan-out, so the pynetbox session is not shared between threads and
NETBOX_MAX_CONNECTIONS is respected.
Each session runs a small, stdlib-only Python collector that is piped to the
remote python3 - over stdin and prints one JSON document. The collector only
gathers raw command output and sysfs/DB values; all parsing and normalisation
happens on the manager. That keeps the remote part trivial, makes every parser
unit-testable against captured fixtures, and a --debug run can dump the raw
document for a bug report.
Linux nodes
Connection as in the existing report commands (OPERATOR_USER, operator key,
resolve_host_with_fallback()), with BatchMode=yes so a missing key fails
instead of blocking a worker thread. The collector runs under sudo
(ethtool -m needs root; report memory already relies on passwordless sudo).
Physical ports are the entries of /sys/class/net/* that have a device
link and type 1 (Ethernet). Bonds, bridges, VLANs and other virtual devices
never have a device link. Switchdev representors are skipped.
Two groups of ports are left out by default and can be added on request:
--include-vf: SR-IOV virtual functions (device/physfnexists). A VF row
carries its own state, MAC and driver and names its physical function
(interface.parent); the transceiver belongs to the physical function and
is not repeated on the VF.--include-infiniband: InfiniBand ports (type32). Link state and rate
come from/sys/class/infiniband/<dev>/ports/<n>/{state,rate}; module
EEPROM access depends on the driver and is best effort.
Each row has interface.kind (physical, vf, infiniband) so the added
rows can be told apart and filtered.
| Data | Source |
|---|---|
| admin state, oper state, MTU, MAC, bond membership, altnames | ip -j -d link show |
| permanent MAC (bond members report the bond's MAC otherwise) | ethtool -P <if> |
| speed, duplex, autoneg, port type, link detected, supported modes | ethtool <if>, /sys/class/net/<if>/{speed,duplex,carrier} |
| FEC | ethtool --show-fec <if> |
| driver, driver version, NIC firmware version, PCI address | ethtool -i <if> |
| NIC vendor, model, subsystem vendor/model, revision (names and IDs) | lspci -D -vmm -nn -s <pci-address> |
| NIC serial number, board ID, running/stored firmware components | devlink -j dev info pci/<pci-address> (best effort, driver dependent) |
| transceiver EEPROM and DOM | ethtool -m <if> |
| LLDP neighbour (switch, port) | lldpctl -f json (best effort, already used by report lldp) |
| system vendor, model, serial | /sys/class/dmi/id/{sys_vendor,product_name,product_serial} |
ethtool -m prints Key : Value lines; JSON output for it only exists in very
recent ethtool releases, so the text form is parsed. A port without a cage
(RJ45, backplane) or with an empty cage makes ethtool -m fail; that is a
normal result (transceiver.present = false), not an error.
A transceiver firmware version only exists for CMIS modules (QSFP-DD, OSFP,
some QSFP28/SFP-DD) and is only printed by newer ethtool versions. SFF-8472 and
SFF-8636 modules have a vendor revision but no firmware field. The report shows
what the module exposes and leaves the field empty otherwise.
SONiC switches
Address and user: local_context_data.management.{ip,username} when present,
else OOB IP (get_device_oob_ip()), else primary IP; user defaults to admin.
Authentication with the operator key, as in SonicCommandBase.
The data is read from the SONiC Redis databases instead of parsing the
human-readable show tables:
| Data | Source |
|---|---|
| port list, alias, configured speed, admin state, FEC, MTU, description, lanes, index | CONFIG_DB PORT|EthernetN |
| oper state, operational speed | APPL_DB PORT_TABLE:EthernetN |
| transceiver vendor, model, serial, revision, type, connector, cable length, compliance, vendor date/OUI | STATE_DB TRANSCEIVER_INFO|EthernetN |
| transceiver firmware (CMIS) | STATE_DB TRANSCEIVER_INFO (active_firmware, inactive_firmware) or TRANSCEIVER_FIRMWARE_INFO|EthernetN, depending on the release |
| DOM: temperature, voltage, per-lane RX/TX power and bias | STATE_DB TRANSCEIVER_DOM_SENSOR|EthernetN |
| HWSKU, platform, base MAC, hostname | CONFIG_DB DEVICE_METADATA|localhost |
| chassis vendor, product name, part number, serial, ONIE version | STATE_DB EEPROM_INFO|*, fallback show platform syseeprom |
| SONiC version, ASIC | /etc/sonic/sonic_version.yml, fallback show version |
| BIOS/CPLD/ONIE firmware | show platform firmware status (best effort) |
A port without an entry in TRANSCEIVER_INFO has no module plugged in. Only
front-panel ports (Ethernet*) are reported; the management port eth0 has no
cage.
The field names in TRANSCEIVER_INFO differ between community SONiC releases
and Enterprise SONiC (for example manufacturer vs. vendor_name,
hardware_rev vs. vendor_rev). The normaliser therefore works with a list of
candidate keys per output field instead of fixed names.
First implementation step: capture the raw collector output on one switch
per supported platform family and SONiC release in use
(DEFAULT_SONIC_VERSION is 4.5.0, the firmware prefix points at Enterprise
SONiC) and on one node per NIC family (Mellanox/NVIDIA, Intel, Broadcom), and
commit the captures as test fixtures. The exact access method on the switch
(sonic-db-dump -n <DB> -y -k '<pattern>', sonic-db-cli, or the
swsscommon Python binding from inside the collector) and the exact key names
have to be confirmed there; they were not verified against a live device for
this issue.
Interface name mapping
The NetBox interfaces of all selected devices are fetched in bulk
(dcim.interfaces.filter(device_id=[...]), chunked), skipping mgmt_only,
virtual and LAG interfaces. Each report row records the NetBox interface name
and how it was matched (mapping), so a wrong or missing match is visible.
SONiC, in this order:
port-config: runconvert_netbox_interface_to_sonic()over the device's
NetBox interfaces and index the results by SONiC name. Needs
sonic_parameters.hwskuand the matchingport_configfile. Results that
do not look likeEthernetNare discarded (the function returns its input
on failure).alias: the livealiasof the port equals the NetBox interface name.name: the NetBox interface is already calledEthernetN.
Linux, in this order:
mac: permanent MAC of the port equals a MAC of the NetBox interface
(mac_address, and themac_addresseslist on NetBox 4.2 and later). This
is the same key Ironic port creation uses.name: kernel name or one of its altnames equals the NetBox interface
name.label: kernel name equals the NetBox interfacelabel.lldp(optional, when LLDP data is available): the LLDP neighbour
(switch, port) equals the cable peer of exactly one NetBox interface.
Rows without a match get mapping = none. NetBox interfaces without a live
port are reported per device as unmatched_netbox_interfaces (JSON) and as a
warning (table/CSV); both directions point at documentation drift.
When a NetBox interface is matched and has a cable, its peer device and peer
interface are added to the row. That makes it possible to put the optics on
both ends of a link side by side.
Data model
One record per physical port. Field names below are the JSON keys; CSV and
table use the same names as flat columns (transceiver.vendor becomes
transceiver_vendor).
device name, inventory_hostname, kind (node|switch), address,
site, rack, role, device_type, manufacturer,
serial, system_vendor, system_model,
# switch only
hwsku, platform, asic, os_version, firmware{bios,cpld,onie}
interface name (as on the device), kind, parent, alias,
netbox_name, mapping,
netbox_peer_device, netbox_peer_interface,
lldp_neighbor, lldp_neighbor_port,
admin_status, oper_status, speed, duplex, autoneg, fec,
mtu, mac, permanent_mac, port_type, bond
nic (node only) pci_address, vendor, model, subsystem_vendor,
subsystem_model, revision, serial, driver,
driver_version, firmware_version, firmware{...}
transceiver present, form_factor, connector, vendor, vendor_oui,
part_number, serial, revision, date_code,
firmware_active, firmware_inactive, compliance,
cable_length, wavelength
transceiver.dom temperature, voltage, lanes[{rx_power, tx_power, tx_bias}]
(only with --dom)
Speeds are normalised to Mbit/s on both device kinds.
Output
--format table(default):tabulatewithpsqlstyle like the sibling
commands, a compact default column set (device, interface, NetBox interface,
admin, oper, speed, form factor, vendor, part number, serial, revision,
firmware, NIC model, NIC firmware) and the usual footer (date, devices,
ports, modules present).--longshows all columns,--column NAME(repeatable) selects columns
explicitly, followingosism/commands/redfish.py.--format csv:csvmodule, header row, flat columns, no footer.--format json: nested per device, so one device's entry is the complete
hardware view:{ "generated_at": "2026-11-11T12:00:00Z", "devices": [ {"name": "...", "kind": "switch", "...": "...", "interfaces": [{"name": "Ethernet0", "netbox_name": "Eth1/1", "transceiver": {"present": true, "...": "..."}}], "unmatched_netbox_interfaces": []} ], "failed": [{"name": "...", "error": "timeout"}] }--output FILEwrites the report to a file instead of stdout.- Row filters applied after collection:
--interface PATTERN(matches the
device-side or the NetBox name),--present,--oper-status. - Rows are sorted by device name and natural interface order (
Ethernet2
beforeEthernet10).
Error handling
- Unreachable device, timeout, authentication failure, unparsable output: the
device goes to thefailedlist, the others are still reported, a summary
warning is logged at the end (as the existing report commands do). - A missing optional tool (
devlink,lldpctl,lspci) leaves the dependent
fields empty and is logged once per device at debug level. - Exit code:
1when selection fails (inventory, NetBox) or no device
matches;0when at least the selection worked.--strictturns any failed
device into exit code1, for use in scripts. - The command is read-only on the devices and in NetBox.
Implementation outline
| File | Change |
|---|---|
setup.cfg |
entry point report transceiver |
osism/commands/report.py |
Transceiver(Command): arguments, orchestration, rendering |
osism/utils/hardware/selection.py (new) |
NetBox query building, classification, name patterns, inventory fallback |
osism/utils/hardware/collect.py (new) |
parallel SSH execution, per-device timeout, failure capture |
osism/utils/hardware/remote_linux.py, remote_sonic.py (new) |
stdlib-only collectors piped to the device |
osism/utils/hardware/parse_linux.py, parse_sonic.py (new) |
parsers/normalisers (ethtool, ethtool -m, lspci -vmm, devlink, SONiC DB tables) |
osism/utils/hardware/mapping.py (new) |
NetBox interface matching for both device kinds |
osism/utils/hardware/render.py (new) |
table / CSV / JSON, column selection |
osism/utils/ssh.py |
shared run_remote() helper (subprocess ssh, BatchMode=yes, stdin payload, timeout) |
tests/unit/commands/test_report.py, tests/unit/utils/hardware/ |
unit tests with captured fixtures |
Suggested order, each step a reviewable pull request:
- Fixture capture on real hardware, parsers and data model with tests.
- Node collection end to end (inventory selection, table/CSV/JSON).
- SONiC collection.
- NetBox selection filters and name patterns.
- Interface mapping, cable peers, unmatched interfaces.
- DOM readings, user documentation.
Acceptance criteria
-
osism report transceiverwithout arguments reports every physical port
of all managed nodes and SONiC switches, collected live. - Each row carries port state (admin, oper, speed), transceiver identity
(vendor, part number, serial, revision, firmware where exposed) and, for
nodes, NIC vendor, model, driver and firmware; switch rows carry
platform, HWSKU, ASIC and SONiC version. - Ports without a module are reported with
present = falseand do not
count as failures. -
--site,--role,--rack,--location,--region,--site-group,
--tenant,--tag,--manufacturer,--device-type,--platform,
--statusand--filternarrow the selection; options are repeatable
and combinable. - Positional name patterns and
--limitselect devices by name;--type
restricts to nodes or switches. - SONiC switches outside
NETBOX_FILTER_CONDUCTOR_SONICare skipped with
a warning, also when named explicitly, and are queried with
--include-unmanaged. - SR-IOV virtual functions and InfiniBand ports are absent by default and
appear with--include-vf/--include-infiniband, marked by
interface.kind. - Per-device failures leave the exit code at
0with a warning;
--strictturns them into exit code1. - Every row shows the NetBox interface name and the mapping method, or
none; NetBox interfaces without a live port are reported. -
--format table|csv|json,--column,--longand--outputwork;
CSV and JSON on stdout contain no log lines. - Devices are queried in parallel; one unreachable device neither blocks
nor aborts the report and is listed as failed. - Without NetBox the command still reports nodes from the inventory and
rejects NetBox filters with a clear message. - Parsers are covered by unit tests using captured output from at least
one node per NIC family in use and one switch per supported SONiC
platform family.
Decisions
- Command name:
osism report transceiver. - Exit code on partial failure:
0with a warning, like the sibling
report commands;--strictmakes any failed device exit1. - Unmanaged SONiC switches: a switch that has a SONiC role but does not
matchNETBOX_FILTER_CONDUCTOR_SONICis not queried, not even when named
explicitly.--include-unmanagedoverrides that. - Virtual functions and InfiniBand ports: not reported by default,
available through--include-vfand--include-infiniband. Switchdev
representors stay excluded.
Out of scope
- Writing the collected data back to NetBox (inventory items / modules) and
flagging differences between NetBox and reality (speed, type). The report
is the prerequisite; both are natural follow-ups. - Switches that do not run SONiC, and out-of-band collection through the BMC.
osism manage redfish list <host> NetworkAdaptersalready covers the
Redfish view for nodes that are powered off. - Persisting reports, an API endpoint, or a frontend view.
- Moving the four existing report commands to the shared SSH/selection
helpers. Worth doing afterwards, but not part of this change. - User documentation in
osism/osism.github.io, to follow once the command
is merged.
Activity