Repository navigation
Description
Category: feature | Scope: Large | Related: #2876
Summary
Turn osism report lldp into a cabling check. The command collects the LLDP
neighbours of every physical port live over SSH, on OSISM-managed Linux
nodes and on SONiC switches, and compares what is actually plugged in with the
cabling documented in NetBox. Each port gets a verdict, so a missing cable, a
cable in the wrong port and two swapped cables are visible at a glance.
Devices are selectable with the same NetBox filters and name patterns as
osism report transceiver (#2876). The comparison with NetBox can be switched
off; the command is then a plain LLDP neighbour report, as today, but for
nodes and switches and with CSV/JSON output.
Motivation
Cabling errors are the most common fault after racking and after hardware
replacement, and they are slow to find: a swapped pair of cables still gives
link on both ports, the node often still boots, and the symptom shows up
later as a BGP session to the wrong leaf, a broken bond or asymmetric traffic.
NetBox already holds the intended cabling, and both the nodes and the switches
already speak LLDP. What is missing is the comparison.
Today osism report lldp prints the raw neighbours of the nodes only. Checking
a rack means reading that table next to NetBox, translating EthernetN/alias
names by hand, and logging in to the switches separately.
Analysis of the current code base
osism report lldp today
osism/commands/report.py, class Lldp:
- Hosts come from
ansible-inventory --list [--limit], so only Linux nodes. - One
ssh ... lldpctl -f jsonper host, serially, 30 s timeout, as
settings.OPERATOR_USERwithout sudo. - The parser already handles the
lldpctl -f jsonquirk that one interface is
a dict and several are a list. - Output is a
tabulatetable with Host, Local Interface, Remote Switch,
Remote Port, Port Description, Age. No other format, no NetBox access. - Covered by
tests/unit/commands/test_report.py(test_lldp_*).
What the switches advertise
The SONiC config generator determines what a node sees as its neighbour:
files/sonic/config_db.jsonenables thelldpfeature and sets
intf_naming_modetostandard.osism/tasks/conductor/sonic/config_generator.pysets every port'salias
throughconvert_sonic_interface_to_alias(), i.e. to the NetBox-style name
(Eth1/1,Eth1/1/3for breakout ports), not to the vendor alias from the
port_configfile (twentyFiveGigE1).- The hostname in
DEVICE_METADATAisget_device_hostname(device): the
custom fieldinventory_hostname, else the NetBox device name. - No port description is generated.
SONiC advertises the port alias as LLDP port ID. On a switch configured by
OSISM the remote port a node sees should therefore already be the NetBox
interface name. Switches that were configured differently advertise
EthernetN or the vendor alias; both can be translated with the existing
helpers in osism/tasks/conductor/sonic/interface.py
(convert_netbox_interface_to_sonic(), get_port_config()).
Cabling in NetBox
osism/tasks/conductor/sonic/connections.py already reads cabling the right
way for this purpose: _get_connected_endpoint() and
get_connected_device_via_interface() use interface.connected_endpoints
together with connected_endpoints_reachable. That is the far end of the
complete cable path, through patch panels, which is exactly the device an
LLDP frame comes from. link_peers would return the patch panel.
Those helpers work on one device at a time through the per-thread interface
cache and return only the device. The check needs the peer interface as
well and has to load many devices, so it should fetch the interfaces in bulk
(dcim.interfaces.filter(device_id=[...]), chunked) and read the endpoints
from the result.
Shared groundwork with #2876
Everything around the actual comparison is the same as for
report transceiver: NetBox-driven device selection and classification into
nodes and switches, name patterns, inventory fallback without NetBox, one SSH
session per device run in parallel, SONiC address resolution, mapping of
device-side interface names to NetBox interfaces, and table/CSV/JSON
rendering. This issue assumes the helpers proposed there under
osism/utils/hardware/. Whichever command is implemented first introduces
them; the second one reuses them.
Proposed command
osism report lldp [NAME ...]
[--no-compare]
[--type {all,node,switch}] [--include-unmanaged] [--no-mgmt]
[-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] [--result RESULT] [--problems]
[--format {table,csv,json}] [--column NAME] [--long] [--output FILE]
[--parallel N] [--timeout SECONDS] [--strict]
Examples:
# check everything, show only what is wrong
osism report lldp --problems
# one rack after hardware was replaced
osism report lldp --rack R12 --site fra1
# all compute nodes, fail the calling script on any finding
osism report lldp -l compute --problems --strict
# plain neighbour listing of the spines, no NetBox comparison
osism report lldp --role spine --no-compare --format csv
Example output:
+-------------+-----------+------------------+------------------+--------------+----------------------------+
| Device | Interface | Expected | Seen | Result | Hint |
|-------------+-----------+------------------+------------------+--------------+----------------------------|
| compute-041 | ens1f0 | leaf-1a:Eth1/11 | leaf-1a:Eth1/11 | ok | |
| compute-041 | ens1f1 | leaf-1b:Eth1/11 | leaf-1b:Eth1/11 | ok | |
| compute-042 | ens1f0 | leaf-1a:Eth1/12 | leaf-1b:Eth1/12 | swapped | swapped with ens1f1 |
| compute-042 | ens1f1 | leaf-1b:Eth1/12 | leaf-1a:Eth1/12 | swapped | swapped with ens1f0 |
| compute-043 | ens1f0 | leaf-1a:Eth1/13 | | missing | link down |
| compute-043 | ens1f1 | leaf-1b:Eth1/13 | leaf-1b:Eth1/14 | wrong-port | documented at compute-044 |
| compute-044 | ens2f0 | | leaf-1a:Eth1/20 | unexpected | no cable in NetBox |
+-------------+-----------+------------------+------------------+--------------+----------------------------+
Date: 2026-11-11 12:00:00
Devices: 4 Ports: 7
ok: 2 swapped: 2 missing: 1 wrong-port: 1 unexpected: 1
Device selection and filtering
Identical to #2876, including the decisions taken there:
- NetBox query with
status=activeplus the typed filter options and
--filter KEY=VALUE; positionalNAMEpatterns (fnmatch) on device name
and inventory hostname;--limitas Ansible pattern for nodes;--type. - A device is a switch when its role is in
DEFAULT_SONIC_ROLESand it
matchesNETBOX_FILTER_CONDUCTOR_SONIC; other SONiC-role switches are
skipped with a warning unless--include-unmanagedis given. A device is a
node when its hostname is in the Ansible inventory. - Only the selected devices are logged in to. The peer of a selected port is
not queried just because it is the peer; it only contributes its own view
when it is part of the selection as well. -l/--limitkeeps working as it does today.
Collection
One SSH session per device, devices in parallel (--parallel, default 10,
--timeout per device, default 60 s).
| Linux node | SONiC switch | |
|---|---|---|
| Neighbours | lldpctl -f json (as today) |
APPL_DB LLDP_ENTRY_TABLE:<port>, fallback lldpctl -f json inside the lldp container |
| Link state per port | ip -j link show / /sys/class/net/<if>/operstate |
APPL_DB PORT_TABLE:<port> oper_status; /sys/class/net/eth0/operstate for the management port |
| Identity for mapping | kernel name, altnames, permanent MAC (ethtool -P) |
port name, alias from CONFIG_DB PORT; MAC of eth0 |
Per neighbour the normalised record holds system name, chassis ID, port ID
with its subtype, port description, management addresses and age. A port can
have more than one neighbour (hypervisor bridges, NIC firmware agents); all
are kept and the comparison treats the port as matching when one of them is
the expected peer.
The link state is collected because it changes the meaning of "no neighbour":
link down means no cable or a dead peer, link up means the peer is silent.
Expected cabling from NetBox
For every selected device the interfaces are loaded in bulk. Virtual and LAG
interfaces are ignored (LLDP runs on the member ports, and NetBox cables are
attached to them).
Management interfaces (mgmt_only) are part of the default run, so the
out-of-band cabling of the switches is checked as well. The rule is the same
for both device kinds: a mgmt_only interface is checked when it has a live
counterpart on the device. On a switch that is the management port eth0,
matched by MAC or name, or as the only mgmt_only interface of the device. A
node's BMC port has no counterpart in the operating system and is skipped
silently instead of being reported as missing; its cable is still verified
from the other side when the OOB switch it is plugged into is a selected SONiC
switch, because there it is an ordinary front-panel port. --no-mgmt leaves
all mgmt_only interfaces out.
The expectation for an interface is its connected_endpoints:
- reachable endpoint of type
dcim.interface: expected peer device and
interface. Several endpoints (breakout cable modelled on one interface) are
all accepted. cableset but the path does not end at a device interface (ends at a
patch panel, circuit or provider network): no expectation, flagged as an
incomplete path in the hint.- no cable: nothing expected.
Resolving LLDP identities to NetBox
Neighbour device: the system name is compared case-insensitively, as given
and with the domain stripped, against NetBox device names and the
inventory_hostname custom field. Fallbacks are the management address
against primary/OOB IPs and the chassis ID against interface MACs. A
neighbour that resolves to nothing is reported as it announced itself.
Neighbour port, on the resolved device, first match wins:
- port ID or port description equals a NetBox interface name or label,
- the remote device is a SONiC switch and the port ID is
EthernetNor a
vendor alias: translate through the HWSKUport_configand the index
built withconvert_netbox_interface_to_sonic(), - port ID is a MAC address (the lldpd default on Linux): equals a MAC of a
NetBox interface, - the remote device was queried as well: match against its live interface
list (name, altnames, permanent MAC, alias) and take the NetBox interface
that live interface is mapped to.
Local port: the mapping defined in #2876 (port-config/alias/name for
switches, mac/name/label for nodes).
Comparison
One result per physical port of a selected device:
| Result | Meaning |
|---|---|
ok |
the neighbour seen is the device and interface NetBox documents |
missing |
NetBox documents a cable, link is down, no neighbour |
no-lldp |
NetBox documents a cable, link is up, no neighbour: probably cabled, but not verifiable (peer does not speak LLDP, or a NIC firmware agent consumes the frames) |
wrong-port |
the expected device, but another interface |
wrong-device |
a different device, or one unknown to NetBox |
swapped |
a wrong-port/wrong-device finding that is explained by exchanging this cable with one or more others; the partners are named in the hint |
unexpected |
a neighbour is seen but NetBox documents no cable on this interface |
one-sided |
this side sees the expected peer, the peer was queried too and does not see this side |
unmapped |
the port has a neighbour but could not be matched to a NetBox interface, so there is nothing to compare with |
stale |
a neighbour entry on a port whose link is down (not yet aged out) |
Ports with no cable in NetBox, no neighbour and no link are not listed unless
--long is given.
Swap detection works on the whole selection, not per device. For a mismatching
interface I that sees endpoint N, look up the interface J for which
NetBox documents N. If J in turn sees the endpoint documented for I, the
two are swapped. Longer rotations are found by following that chain. This
covers two ports crossed on one node as well as the cables of two neighbouring
nodes crossed on the switch. When J was not queried the finding stays
wrong-port/wrong-device and the hint says where the seen cable is
documented.
The comparison is a pure function over plain data (expected endpoints,
observed neighbours, link states), independent of SSH and pynetbox, so the
whole result matrix is unit-testable.
Without the NetBox comparison
--no-compare skips the comparison; it is also the automatic behaviour when
NetBox is not configured (then only nodes from the inventory can be queried,
as in #2876). The output is the neighbour listing with the columns of today's
command. NetBox filters still work with --no-compare when NetBox is
available, because selection and comparison are independent.
Two checks need no source of truth and stay active: one-sided when both ends
were queried, and no-lldp for ports with link but without neighbour.
Output
--format table(default),csv,json;--column,--long,
--output FILE; rows sorted by device and natural interface order.- Table footer: date, devices, ports, and the count per result.
--problemshidesok;--result RESULT(repeatable) selects specific
results;--interface PATTERNfilters by device-side or NetBox name.- JSON is nested per device:
{ "generated_at": "2026-11-11T12:00:00Z", "compared_with_netbox": true, "summary": {"ok": 2, "swapped": 2, "missing": 1}, "devices": [ {"name": "compute-042", "kind": "node", "interfaces": [ {"name": "ens1f0", "netbox_name": "ens1f0", "mapping": "mac", "oper_status": "up", "expected": {"device": "leaf-1a", "interface": "Eth1/12"}, "seen": [{"device": "leaf-1b", "interface": "Eth1/12", "system_name": "leaf-1b", "port_id": "Eth1/12", "chassis_id": "...", "age": "3 days"}], "result": "swapped", "hint": "swapped with ens1f1"}]} ], "failed": [{"name": "...", "error": "timeout"}] }
Error handling and exit code
- An unreachable device goes to the
failedlist; the report continues. Its
ports can still appear from the other side when the peers were queried. lldpdnot running orlldpctlmissing is a per-device failure with a
clear message, not an empty neighbour list.- Exit code as decided for Add report transceiver command: live NIC and transceiver hardware inventory for Linux nodes and SONiC switches #2876:
1when selection fails or nothing
matches, otherwise0.--strictreturns1when a device failed or any
port has a result other thanok. - The command is read-only on the devices and in NetBox.
Implementation outline
| File | Change |
|---|---|
osism/commands/report.py |
rework Lldp: arguments, orchestration, rendering |
osism/utils/hardware/selection.py, collect.py, mapping.py, render.py |
shared with #2876 (created by whichever lands first) |
osism/utils/hardware/parse_lldp.py (new) |
normalise lldpctl JSON and LLDP_ENTRY_TABLE into one neighbour record |
osism/utils/hardware/cabling.py (new) |
expected endpoints from NetBox interfaces, identity resolution, comparison, swap detection |
tests/unit/commands/test_report.py |
adapt test_lldp_*, add command-level tests |
tests/unit/utils/hardware/ |
parser fixtures and the comparison matrix |
Suggested order, each step a reviewable pull request:
- Capture real LLDP data (see below), neighbour normalisation for nodes and
switches,--no-comparelisting with selection, parallel collection and
output formats. - Expected cabling from NetBox, identity resolution, results
ok,
missing,no-lldp,wrong-port,wrong-device,unexpected,
unmapped. - Swap detection,
one-sided,stale, hints,--problems,--strict.
To confirm on real hardware first (not verified against a live device for
this issue), and to commit as fixtures:
- which port ID and subtype a SONiC switch configured by OSISM advertises
(expected: the alias,Eth1/1), including breakout ports, - which port ID and port description
lldpdadvertises on the nodes as
deployed by OSISM (MAC or interface name), in particular for bond members, - whether the system name arrives short or fully qualified,
- the field names of
LLDP_ENTRY_TABLEon the SONiC release in use, and
whether the neighbour of the management porteth0is listed there.
Acceptance criteria
-
osism report lldpcollects LLDP neighbours live from managed nodes
and SONiC switches, in parallel. - With NetBox available, every port of a selected device that has a cable
in NetBox or a live neighbour gets one of the results above. - A removed cable is reported as
missing, a cable in the wrong port as
wrong-port, a cable to another device aswrong-device, an
undocumented cable asunexpected. - Two exchanged cables are reported as
swappedon both ports with the
partner named, for a swap within one device and across two devices. - Cable paths through patch panels are compared with the far-end device.
- The management port of a switch is checked in the default run; BMC
ports of nodes produce no finding on the node;--no-mgmtexcludes
allmgmt_onlyinterfaces. - Only selected devices are logged in to; documented peers outside the
selection are not queried. -
--no-compareproduces the neighbour listing without NetBox cabling
data; without NetBox configured this is the automatic behaviour. - The selection options of Add report transceiver command: live NIC and transceiver hardware inventory for Linux nodes and SONiC switches #2876 (
NAME,--limit,--type, NetBox
filters,--filter,--include-unmanaged) work identically. -
--problems,--resultand--interfacefilter the rows;
--format table|csv|json,--column,--long,--outputwork and
CSV/JSON on stdout contain no log lines. -
--strictexits1on any finding or failed device. - One unreachable device neither blocks nor aborts the report.
- The comparison and swap detection are covered by unit tests over a
result matrix; the parsers by fixtures captured from real devices.
Decisions
- Command: the existing
osism report lldpis extended; no separate
command is added. - Default: the comparison with NetBox is on whenever NetBox is
configured;--no-compareturns it off. The default table of the command
changes accordingly. - Peers outside the selection are not logged in to, and there is no
option to expand the selection to them.one-sidedand swap detection
work on what was selected. - Management interfaces: the OOB cabling of the switches is part of the
default run;--no-mgmtexcludes it. - Silent peers: devices that never send LLDP end up as
no-lldp. That
is sufficient; no mechanism to exclude such interfaces is added.
Out of scope
- Changing anything in NetBox (creating or correcting cables from the
observed state). - Rule-based checks that are not documented in NetBox, such as "both members
of a bond must end on different switches". Useful as a follow-up on top of
the collected data. - Devices that are neither managed nodes nor SONiC switches, and collecting
LLDP through the BMC. - Changing the LLDP configuration of nodes or switches.
Activity
Collect LLDP neighbours on SONiC switches
Add NetBox device lookups and the vendor alias index
Add the comparison of LLDP neighbours with NetBox cabling
Extend report lldp into a cabling check
Collect LLDP neighbours on SONiC switches
Add NetBox device lookups and the vendor alias index
Add the comparison of LLDP neighbours with NetBox cabling
Extend report lldp into a cabling check