Repository navigation
Description
The stack #2879 → #2880 → #2881 (osism report transceiver, cabling check in osism report lldp, part of #2876 and #2877) was written without access to a device. Its parsers are tested against synthetic fixtures in tests/unit/utils/hardware/fixtures/, written from documentation. Before the stack is merged, these fixtures have to be replaced by captures from real Linux nodes and SONiC switches, and a few assumptions have to be checked against real output.
This issue lists the devices and the commands needed for that.
Devices
One capture per row, as far as the deployment has such a device.
Linux nodes
- Node with a Mellanox/NVIDIA ConnectX NIC (replaces
linux-mellanox.json) - Node with an Intel NIC (replaces
linux-intel.json) - Node with a Broadcom NIC (replaces
linux-broadcom.json) - Node with SR-IOV VFs enabled and/or InfiniBand ports (replaces
linux-mellanox-vf-ib.json)
Across these nodes, the following ports should appear at least once: an optical module (SFP28 or QSFP28), a DAC, a CMIS module (QSFP-DD or OSFP, if any are in use), an empty cage, an RJ45 or onboard port, and a bond member.
SONiC switches
- One switch per platform family and SONiC release in use, Enterprise SONiC 4.5.0 first (replaces
sonic-enterprise.json) - Community SONiC, if in use anywhere (replaces
sonic-community.json)
Across the switches, the following ports should appear at least once: a breakout port, an empty cage, an optical module, a DAC, a CMIS module (if any are in use), and eth0 cabled to the OOB switch.
Both ends of a cable
- At least one node and the switch it is cabled to, captured at the same time, including a bond. The LLDP check compares what each side advertises with what the other side sees, so both ends of the same cables are needed.
For every device, note its NetBox device name if it differs from the host name.
1. Collector documents
These are the documents the reports work on. They become the new fixtures directly.
The collectors need nothing from the stack installed. Save the two scripts from the appendix ("Collector scripts") at the end of this issue as remote_linux.py and remote_sonic.py, or fetch the current version from the branch:
BRANCH=implement/issue-2877-report-lldp-cabling-check
BASE=https://raw.githubusercontent.com/osism/python-osism/$BRANCH/osism/utils/hardware
curl -fsSLO "$BASE/remote_linux.py"
curl -fsSLO "$BASE/remote_sonic.py"They are piped to python3 - over SSH, exactly as the reports do it:
# Linux node, as `osism report transceiver` runs it (as root)
ssh dragon@NODE 'sudo -n python3 - --sections ports,lldp,ethtool,module,nic,system --include-vf --include-infiniband' \
< remote_linux.py > NODE.transceiver.json
# Linux node, as `osism report lldp` runs it (without root)
ssh dragon@NODE 'python3 - --sections ports,lldp' \
< remote_linux.py > NODE.lldp.json
# SONiC switch, all sections (user: management.username from the local context, default admin)
ssh admin@SWITCH 'python3 - --sections ports,transceiver,dom,system,lldp' \
< remote_sonic.py > SWITCH.jsonIf a collector fails, its stderr is the result we need. Please attach it instead of the JSON.
Where the stack is installed on a manager, --dump-raw writes the same documents:
osism report transceiver --dom --include-vf --include-infiniband --dump-raw /tmp/capture NODE SWITCH
osism report lldp --dump-raw /tmp/capture-lldp NODE SWITCH2. Individual commands
The output of the commands the collectors run, plus the human-readable views and the LLDP configuration. This is needed to check the collectors themselves, and for the points the collectors do not cover (see "Open questions").
Linux node
Run as root on the node: sudo bash capture-linux.sh > "$(hostname)-commands.txt" 2>&1
#!/bin/bash
run() { echo "### \$ $*"; "$@"; rc=$?; echo "### rc=$rc"; echo; return $rc; }
run hostname
run hostname -f
run cat /etc/os-release
run uname -r
run python3 --version
run ethtool --version
run lldpcli -v
run lspci -D -nn
run ip -j -d link show
run ip -d link show
run cat /proc/net/bonding/*
run grep -H . /sys/class/dmi/id/sys_vendor /sys/class/dmi/id/product_name /sys/class/dmi/id/product_serial
for dev in /sys/class/net/*; do
name=$(basename "$dev")
[ -e "$dev/device" ] || continue
pci=$(basename "$(readlink -f "$dev/device")")
echo "######## $name ($pci)"
run grep -H . "$dev"/{type,address,carrier,dev_port,duplex,mtu,operstate,phys_port_name,speed}
run ethtool "$name"
run ethtool -i "$name"
run ethtool -P "$name"
run ethtool --show-fec "$name"
run ethtool -m "$name"
run lspci -D -vmm -nn -s "$pci"
run devlink -j dev info "pci/$pci"
[ -e "$dev/device/physfn" ] && run ls "$dev/device/physfn/net"
for port in "$dev"/device/infiniband/*/ports/*; do
[ -e "$port" ] && run grep -H . "$port/state" "$port/rate"
done
done
run lldpctl -f json
run lldpcli show chassis -f json
run lldpcli show interfaces -f json
run lldpcli show configurationSONiC switch
Run as the login user (admin), not as root: bash capture-sonic.sh > "$(hostname)-commands.txt" 2>&1. The script tries every database dump as the login user first and with sudo -n second, like the collector does. Both attempts are kept in the output.
#!/bin/bash
run() { echo "### \$ $*"; "$@"; rc=$?; echo "### rc=$rc"; echo; return $rc; }
dump() { run sonic-db-dump -n "$1" -y -k "$2" || run sudo -n sonic-db-dump -n "$1" -y -k "$2"; }
run id
run hostname
run hostname -f
run command -v python3 sonic-db-dump sonic-db-cli docker
run python3 --version
run cat /etc/sonic/sonic_version.yml
run show version
dump CONFIG_DB 'PORT|*'
dump CONFIG_DB 'DEVICE_METADATA|localhost'
dump APPL_DB 'PORT_TABLE:*'
dump STATE_DB 'TRANSCEIVER_INFO|*'
dump STATE_DB 'TRANSCEIVER_FIRMWARE_INFO|*'
dump STATE_DB 'TRANSCEIVER_DOM_SENSOR|*'
dump STATE_DB 'EEPROM_INFO|*'
dump APPL_DB 'LLDP_ENTRY_TABLE:*'
run grep -H . /sys/class/net/eth0/{address,carrier,mtu,operstate,speed}
run show platform syseeprom
run show platform firmware status
run docker exec lldp lldpctl -f json || run sudo -n docker exec lldp lldpctl -f json
run docker exec lldp lldpcli show chassis -f json || run sudo -n docker exec lldp lldpcli show chassis -f json
run docker exec lldp lldpcli show interfaces -f json || run sudo -n docker exec lldp lldpcli show interfaces -f json
run docker exec lldp lldpcli show configuration || run sudo -n docker exec lldp lldpcli show configuration
# Human-readable views, to cross-check the parsed values
run show interfaces status
run show interfaces alias
run show interfaces breakout
run show interfaces transceiver presence
run show interfaces transceiver eeprom --dom
run show interfaces transceiver info
run show lldp table
run show lldp neighborsSome of the human-readable commands do not exist on every release; their error is recorded with the exit code. On Enterprise SONiC, the output of the equivalent sonic-cli commands (for example show interface transceiver and show lldp neighbor) is welcome as well.
Open questions the captures answer
| Question | Answered by | Pull request |
|---|---|---|
Does sonic-db-dump work as the login user, or only with sudo -n? Does sudo -n work without a password? |
dump lines of capture-sonic.sh, db_access in the collector document |
#2879 |
Key names in TRANSCEIVER_INFO, TRANSCEIVER_FIRMWARE_INFO and TRANSCEIVER_DOM_SENSOR on Enterprise SONiC (and on community SONiC) |
dump STATE_DB ... |
#2880 |
Column layout of show platform firmware status, which is parsed by column position |
show platform firmware status |
#2880 |
Output of ethtool -m for SFF-8472, SFF-8636 and CMIS modules, and for an empty cage |
ethtool -m on the nodes |
#2880 |
Field names of LLDP_ENTRY_TABLE, and whether the neighbour of eth0 is listed there |
dump APPL_DB 'LLDP_ENTRY_TABLE:*' |
#2881 |
Port ID and subtype a SONiC switch configured by OSISM advertises (expected: the alias, e.g. Eth1/1), including breakout ports |
lldpctl -f json on the node, lldpcli show interfaces on the switch |
#2881 |
Port ID and port description lldpd advertises on the nodes (MAC or interface name), in particular for bond members |
LLDP_ENTRY_TABLE and lldpctl on the switch, lldpcli show interfaces on the node |
#2881 |
| Does the system name arrive short or fully qualified? | lldpcli show chassis on both sides, hostname / hostname -f |
#2881 |
Handing over the captures
The captures contain host names, IP and MAC addresses and serial numbers of the deployment. Before attaching them here:
- Replace host names, IP addresses, MAC addresses and serial numbers consistently across all files: the same value always becomes the same replacement. The port matching and the cabling check relate MAC addresses and names between both ends of a cable and NetBox, so inconsistent replacements break the fixtures.
- Keep vendor names, part numbers, firmware versions, key names and the structure of the output as they are.
- Attach one
.tar.gzor.zipper device, or one for everything, containing the collector documents (*.json) and the command output (*-commands.txt).
Once the captures are here, they replace the fixtures in tests/unit/utils/hardware/fixtures/ within the stack, and the tests are adjusted to them.
Appendix: collector scripts
The collectors as of commit 448fd302 of implement/issue-2877-report-lldp-cabling-check. Both use the Python standard library only and run with Python 3.6 or newer.
remote_linux.py
# SPDX-License-Identifier: Apache-2.0
"""Collector for Linux nodes, executed on the node itself.
This file is piped to ``python3 -`` over SSH. It must stay self-contained:
standard library only, no imports from ``osism``, and no syntax newer than
Python 3.6, because it runs with whatever interpreter the node has.
It only gathers raw command output and sysfs values and prints them as one
JSON document. All parsing happens on the manager (``parse_linux``,
``parse_lldp``), where it can be tested against captured documents.
"""
import argparse
import json
import os
import re
import socket
import subprocess
import sys
VERSION = 1
SYS_NET = "/sys/class/net"
SYS_DMI = "/sys/class/dmi/id"
ARPHRD_ETHER = "1"
ARPHRD_INFINIBAND = "32"
SYSFS_ATTRIBUTES = [
"address",
"carrier",
"dev_port",
"duplex",
"mtu",
"operstate",
"phys_port_name",
"speed",
"type",
]
# phys_port_name of a switchdev representor, e.g. pf0vf3 or c1pf0sf2
REPRESENTOR = re.compile(r"pf\d+(vf|sf)\d+$")
def run(argv, timeout=20):
"""Run a command and return its exit code and output as a dict."""
try:
process = subprocess.run(
argv,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
timeout=timeout,
)
except OSError as exc:
return {"rc": 127, "out": "", "err": str(exc)}
except subprocess.TimeoutExpired:
return {"rc": 124, "out": "", "err": "timeout"}
return {
"rc": process.returncode,
"out": process.stdout,
"err": process.stderr.strip(),
}
def read(path):
"""Return the stripped content of a file, or None when it is unreadable."""
try:
with open(path) as handle:
return handle.read().strip()
except (IOError, OSError):
return None
def port_kind(name, sys_net=SYS_NET):
"""Classify a network device.
Returns ``physical``, ``vf`` or ``infiniband`` for a port backed by
hardware, and None for everything else: bonds, bridges, VLANs and other
virtual devices have no ``device`` link, and switchdev representors stand
for a function of the NIC, not for a port.
"""
base = os.path.join(sys_net, name)
if not os.path.exists(os.path.join(base, "device")):
return None
link_type = read(os.path.join(base, "type"))
if link_type == ARPHRD_INFINIBAND:
return "infiniband"
if link_type != ARPHRD_ETHER:
return None
if os.path.exists(os.path.join(base, "device", "physfn")):
return "vf"
if REPRESENTOR.search(read(os.path.join(base, "phys_port_name")) or ""):
return None
return "physical"
def list_ports(kinds, sys_net=SYS_NET):
"""Return ``{name: kind}`` for the ports of the requested kinds."""
try:
names = sorted(os.listdir(sys_net))
except OSError:
return {}
ports = {}
for name in names:
kind = port_kind(name, sys_net)
if kind in kinds:
ports[name] = kind
return ports
def collect_port(name, kind, sys_net=SYS_NET, runner=run):
"""Collect identity and link state of one port."""
base = os.path.join(sys_net, name)
port = {
"kind": kind,
"sysfs": dict(
(attribute, read(os.path.join(base, attribute)))
for attribute in SYSFS_ATTRIBUTES
),
"pci": os.path.basename(os.path.realpath(os.path.join(base, "device"))),
"ethtool_P": runner(["ethtool", "-P", name]),
}
if kind == "vf":
try:
port["physfn"] = sorted(
os.listdir(os.path.join(base, "device", "physfn", "net"))
)
except OSError:
port["physfn"] = []
if kind == "infiniband":
port["infiniband"] = collect_infiniband(base, port["sysfs"].get("dev_port"))
return port
def collect_infiniband(base, dev_port):
"""Collect link state and rate of an InfiniBand port.
The netdev of an IPoIB port only knows its IB device. State and rate
are kept per port of that device, numbered from 1, while ``dev_port``
counts from 0.
"""
directory = os.path.join(base, "device", "infiniband")
try:
devices = sorted(os.listdir(directory))
except OSError:
return {}
if not devices:
return {}
try:
number = int(dev_port) + 1
except (TypeError, ValueError):
number = 1
port = os.path.join(directory, devices[0], "ports", str(number))
return {
"device": devices[0],
"port": number,
"state": read(os.path.join(port, "state")),
"rate": read(os.path.join(port, "rate")),
}
def section_ports(document, ports, sys_net, runner):
document["ip_link"] = runner(["ip", "-j", "-d", "link", "show"])
for name, kind in ports.items():
document["ports"][name].update(collect_port(name, kind, sys_net, runner))
def section_lldp(document, ports, sys_net, runner):
document["lldpctl"] = runner(["lldpctl", "-f", "json"])
def section_ethtool(document, ports, sys_net, runner):
for name in ports:
document["ports"][name].update(
{
"ethtool": runner(["ethtool", name]),
"ethtool_i": runner(["ethtool", "-i", name]),
"ethtool_fec": runner(["ethtool", "--show-fec", name]),
}
)
def section_module(document, ports, sys_net, runner):
# Fails for a port without a cage and for an empty cage. That is a
# result, not an error, and is kept with its exit code.
for name in ports:
document["ports"][name]["ethtool_m"] = runner(["ethtool", "-m", name])
def section_nic(document, ports, sys_net, runner):
# Several ports can share one PCI function, and devlink reports per
# function, so each address is queried once.
cache = {}
for name in ports:
address = os.path.basename(
os.path.realpath(os.path.join(sys_net, name, "device"))
)
if address not in cache:
cache[address] = {
"lspci": runner(["lspci", "-D", "-vmm", "-nn", "-s", address]),
"devlink": runner(["devlink", "-j", "dev", "info", "pci/" + address]),
}
document["ports"][name].update(cache[address])
def section_system(document, ports, sys_net, runner):
document["dmi"] = dict(
(attribute, read(os.path.join(SYS_DMI, attribute)))
for attribute in ("sys_vendor", "product_name", "product_serial")
)
SECTIONS = {
"ports": section_ports,
"lldp": section_lldp,
"ethtool": section_ethtool,
"module": section_module,
"nic": section_nic,
"system": section_system,
}
def collect(sections, kinds, sys_net=SYS_NET, runner=run):
"""Run the requested sections and return the document."""
ports = list_ports(kinds, sys_net)
document = {
"collector": "linux",
"version": VERSION,
"hostname": socket.gethostname(),
"sections": sections,
"ports": dict((name, {}) for name in ports),
}
for section in sections:
SECTIONS[section](document, ports, sys_net, runner)
return document
def main(argv=None, sys_net=SYS_NET, runner=run, stream=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--sections", default="ports")
parser.add_argument("--include-vf", action="store_true")
parser.add_argument("--include-infiniband", action="store_true")
arguments = parser.parse_args(argv)
sections = [name for name in arguments.sections.split(",") if name]
unknown = [name for name in sections if name not in SECTIONS]
if unknown:
sys.stderr.write("unknown section(s): %s\n" % ", ".join(unknown))
return 2
kinds = ["physical"]
if arguments.include_vf:
kinds.append("vf")
if arguments.include_infiniband:
kinds.append("infiniband")
json.dump(collect(sections, kinds, sys_net, runner), stream or sys.stdout)
return 0
if __name__ == "__main__":
sys.exit(main())remote_sonic.py
# SPDX-License-Identifier: Apache-2.0
"""Collector for SONiC switches, executed on the switch itself.
This file is piped to ``python3 -`` over SSH. It must stay self-contained:
standard library only, no imports from ``osism``, and no syntax newer than
Python 3.6, because it runs with the interpreter of the SONiC host system.
It reads the SONiC Redis databases with ``sonic-db-dump`` instead of parsing
the human-readable ``show`` tables, and prints the raw dumps as one JSON
document. All parsing happens on the manager (``parse_sonic``,
``parse_lldp``), where it can be tested against captured documents.
"""
import argparse
import json
import os
import socket
import subprocess
import sys
VERSION = 1
SYS_NET = "/sys/class/net"
MGMT_PORT = "eth0"
VERSION_FILE = "/etc/sonic/sonic_version.yml"
# Tables dumped per section: (database, table, key pattern, mandatory)
TABLES = {
"ports": [
("CONFIG_DB", "PORT", "PORT|*", True),
("CONFIG_DB", "DEVICE_METADATA", "DEVICE_METADATA|localhost", False),
("APPL_DB", "PORT_TABLE", "PORT_TABLE:*", False),
],
"transceiver": [
("STATE_DB", "TRANSCEIVER_INFO", "TRANSCEIVER_INFO|*", False),
# Newer releases keep the CMIS firmware versions in a table of
# their own instead of in TRANSCEIVER_INFO.
(
"STATE_DB",
"TRANSCEIVER_FIRMWARE_INFO",
"TRANSCEIVER_FIRMWARE_INFO|*",
False,
),
],
"dom": [
("STATE_DB", "TRANSCEIVER_DOM_SENSOR", "TRANSCEIVER_DOM_SENSOR|*", False),
],
"system": [
("STATE_DB", "EEPROM_INFO", "EEPROM_INFO|*", False),
],
"lldp": [
("APPL_DB", "LLDP_ENTRY_TABLE", "LLDP_ENTRY_TABLE:*", False),
],
}
def run(argv, timeout=30):
"""Run a command and return its exit code and output as a dict."""
try:
process = subprocess.run(
argv,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
timeout=timeout,
)
except OSError as exc:
return {"rc": 127, "out": "", "err": str(exc)}
except subprocess.TimeoutExpired:
return {"rc": 124, "out": "", "err": "timeout"}
return {
"rc": process.returncode,
"out": process.stdout,
"err": process.stderr.strip(),
}
def read(path):
"""Return the stripped content of a file, or None when it is unreadable."""
try:
with open(path) as handle:
return handle.read().strip()
except (IOError, OSError):
return None
def db_dump(database, pattern, runner=run):
"""Dump the keys of a database matching a pattern.
The dump is tried as the login user first and with ``sudo -n`` second,
because access to Redis is restricted on some SONiC releases.
Returns:
``(dump, access, error)``: the parsed dump with the access method
that worked, or None with the last error
"""
error = "no output"
for access, prefix in (("user", []), ("sudo", ["sudo", "-n"])):
result = runner(prefix + ["sonic-db-dump", "-n", database, "-y", "-k", pattern])
if result["rc"] != 0:
error = result["err"] or "exit code %d" % result["rc"]
continue
try:
dump = json.loads(result["out"] or "{}")
except ValueError:
error = "unparsable sonic-db-dump output"
continue
if isinstance(dump, dict):
return dump, access, None
error = "unparsable sonic-db-dump output"
return None, None, error
def extra_ports(document, sys_net, runner):
base = os.path.join(sys_net, MGMT_PORT)
if os.path.isdir(base):
document["mgmt"] = {
MGMT_PORT: dict(
(attribute, read(os.path.join(base, attribute)))
for attribute in ("address", "carrier", "mtu", "operstate", "speed")
)
}
def extra_system(document, sys_net, runner):
document["files"] = {"sonic_version": read(VERSION_FILE)}
commands = {}
# The show commands are fallbacks for what the files and tables above
# did not deliver, and the only source for the firmware versions.
if not document["files"]["sonic_version"]:
commands["show version"] = runner(["show", "version"])
if not (document["db"].get("STATE_DB") or {}).get("EEPROM_INFO"):
commands["show platform syseeprom"] = runner(["show", "platform", "syseeprom"])
commands["show platform firmware status"] = runner(
["show", "platform", "firmware", "status"]
)
document["commands"] = commands
def extra_lldp(document, sys_net, runner):
# lldpctl is the fallback for a switch without LLDP_ENTRY_TABLE, and the
# second source for the management port in case the table only lists
# front-panel ports. lldpd runs in the lldp container.
entries = (document["db"].get("APPL_DB") or {}).get("LLDP_ENTRY_TABLE") or {}
if "LLDP_ENTRY_TABLE:" + MGMT_PORT in entries:
return
command = ["docker", "exec", "lldp", "lldpctl", "-f", "json"]
result = runner(command)
if result["rc"] != 0:
result = runner(["sudo", "-n"] + command)
document["lldpctl"] = result
EXTRAS = {
"ports": extra_ports,
"system": extra_system,
"lldp": extra_lldp,
}
def collect(sections, sys_net=SYS_NET, runner=run):
"""Run the requested sections and return the document.
Raises:
RuntimeError: A mandatory table could not be read
"""
document = {
"collector": "sonic",
"version": VERSION,
"hostname": socket.gethostname(),
"sections": sections,
"db": {},
"errors": {},
}
for section in sections:
for database, table, pattern, mandatory in TABLES.get(section, []):
dump, access, error = db_dump(database, pattern, runner)
if dump is None:
if mandatory:
raise RuntimeError(
"cannot read %s %s: %s" % (database, table, error)
)
document["errors"]["%s %s" % (database, table)] = error
continue
document["db"].setdefault(database, {})[table] = dump
document["db_access"] = access
extra = EXTRAS.get(section)
if extra:
extra(document, sys_net, runner)
return document
def main(argv=None, sys_net=SYS_NET, runner=run, stream=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--sections", default="ports")
arguments = parser.parse_args(argv)
sections = [name for name in arguments.sections.split(",") if name]
unknown = [name for name in sections if name not in TABLES and name not in EXTRAS]
if unknown:
sys.stderr.write("unknown section(s): %s\n" % ", ".join(unknown))
return 2
try:
document = collect(sections, sys_net, runner)
except RuntimeError as exc:
sys.stderr.write("%s\n" % exc)
return 1
json.dump(document, stream or sys.stdout)
return 0
if __name__ == "__main__":
sys.exit(main())Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsIn progress
Activity
berendt commentedon Nov 13, 2026
Captures of three nodes and two switches were handed over privately. They are not attached here. The stack #2879 → #2880 → #2881 was checked against them and updated; five of the documents are now fixtures in
tests/unit/utils/hardware/fixtures/, anonymized and reduced to a selection of ports.What the captures cover
linux-connectx4.json)linux-82599.json, and in the other two)linux-bcm57414.json)sonic-enterprise-4.4.json,sonic-enterprise.json)Ports on the nodes: DAC and RJ45/onboard ports are there. An optical module, a CMIS module, an empty cage and a bond member are not.
Ports on the switches: breakout ports, empty cages, optical modules (QSFP28 SR4 with diagnostics), DACs and
eth0cabled to the OOB switch are there. A CMIS module is not.Answers to the open questions
sonic-db-dumpwork as the login user?redisgroup. Thesudo -nretry was not needed, so whether it works without a password is still open.TRANSCEIVER_INFO,TRANSCEIVER_FIRMWARE_INFO,TRANSCEIVER_DOM_SENSORTRANSCEIVER_INFOhas the community names (manufacturer,model,serial,vendor_date,vendor_oui,hardware_rev) and names of its own (vendor_name,vendor_part_number,vendor_serial_number,vendor_date_code).specification_complianceis the text of a Python dict,vendor_datean ISO timestamp,cable_lengtha number of metres.TRANSCEIVER_FIRMWARE_INFOis empty without CMIS modules.TRANSCEIVER_DOM_SENSORusesrx1power,tx1bias,tx1power,temperature,voltage; a DAC has an entry withN/Aeverywhere.show platform firmware statusChassis,Module,Component,Version,Description, as assumed. ONIE is not a component on this platform.ethtool -mOperation not supportedfor RJ45 ports. Still open for SFF-8636, CMIS and an empty cage.LLDP_ENTRY_TABLE, andeth0lldp_rem_*names as assumed, subtypes as numbers.eth0is listed. The key isLLDP_ENTRY_TABLE:<port>on 4.4.1 andLLDP_ENTRY_TABLE:<port>:<source MAC of the neighbour>on 4.5.2.lldp_rem_time_markis the uptime oflldpdat which the neighbour was learned, not its age.local, also for breakout ports (Eth1/1/1). The port description is the SONiC name (Ethernet0).lldpdadvertises on the nodesWhat changed in the stack
bridge/ether9; it is compared by the name of the port.TRANSCEIVER_INFOare written the way the Linux parser writes them, so that both ends of a cable read alike (compliance codes, vendor OUI, cable length, wavelength, date code). A DAC is reported without diagnostics. The markerXcvrInitDoneis not a port. The board serial number ofbnxt_enis used.SONiC-OS-is removed from the version ofshow version.LLDP_ENTRY_TABLEare read; before, every neighbour of a 4.5.2 switch was reported a second time under a port name that does not exist. Neighbours from the table have no age. A neighbour that does not announce a device name (another host name, the default name of an image, the LLDP agent of a NIC) is found by its port ID. A MAC address that NetBox documents on several devices does not identify a device: several nodes announce the same chassis ID, the MAC address of the USB host interface of their BMC.Still open
Captures of the following would close the remaining gaps:
sonic-db-dumpordocker exec lldpneedssudo -n.Assisted-by: Claude:claude-fable-5-2