Skip to content

Capture SONiC and Linux command output for the hardware report stack #2884

Open

Description

@berendt

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.json

If 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 SWITCH

2. 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 configuration

SONiC 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 neighbors

Some 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.gz or .zip per 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())

Activity

  1. self-assigned this
    on Nov 12, 2026
  2. moved this from New to In progress in Human Boardon Nov 12, 2026
  3. berendt commented on Nov 13, 2026

    @berendt
    MemberAuthor

    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

    Asked for Covered
    Node with a Mellanox/NVIDIA ConnectX NIC yes, ConnectX-4 Lx (linux-connectx4.json)
    Node with an Intel NIC yes, I350, X710 10GBASE-T and 82599ES (linux-82599.json, and in the other two)
    Node with a Broadcom NIC yes, BCM57414 (linux-bcm57414.json)
    Node with SR-IOV VFs and/or InfiniBand ports no
    Enterprise SONiC yes, 4.4.1 and 4.5.2 on Accton AS7726-32X (sonic-enterprise-4.4.json, sonic-enterprise.json)
    Community SONiC no
    Both ends of a cable yes: nodes to both leaf switches, the link between the leaf switches, management ports to the OOB switch. Without a bond

    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 eth0 cabled to the OOB switch are there. A CMIS module is not.

    Answers to the open questions

    Question Answer
    Does sonic-db-dump work as the login user? Yes, on 4.4.1 and 4.5.2. The login user is a member of the redis group. The sudo -n retry was not needed, so whether it works without a password is still open.
    Key names in TRANSCEIVER_INFO, TRANSCEIVER_FIRMWARE_INFO, TRANSCEIVER_DOM_SENSOR TRANSCEIVER_INFO has 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_compliance is the text of a Python dict, vendor_date an ISO timestamp, cable_length a number of metres. TRANSCEIVER_FIRMWARE_INFO is empty without CMIS modules. TRANSCEIVER_DOM_SENSOR uses rx1power, tx1bias, tx1power, temperature, voltage; a DAC has an entry with N/A everywhere.
    Column layout of show platform firmware status Chassis, Module, Component, Version, Description, as assumed. ONIE is not a component on this platform.
    Output of ethtool -m Confirmed for SFF-8472 passive copper cables. Operation not supported for RJ45 ports. Still open for SFF-8636, CMIS and an empty cage.
    Field names of LLDP_ENTRY_TABLE, and eth0 The lldp_rem_* names as assumed, subtypes as numbers. eth0 is listed. The key is LLDP_ENTRY_TABLE:<port> on 4.4.1 and LLDP_ENTRY_TABLE:<port>:<source MAC of the neighbour> on 4.5.2. lldp_rem_time_mark is the uptime of lldpd at which the neighbour was learned, not its age.
    Port ID a SONiC switch configured by OSISM advertises The alias with the subtype local, also for breakout ports (Eth1/1/1). The port description is the SONiC name (Ethernet0).
    Port ID and description lldpd advertises on the nodes The MAC address of the port, and its name as description. Still open for bond members.
    System name short or fully qualified? Short, on nodes and switches.

    What changed in the stack

    • Add shared helpers for live hardware reports #2879: a port on a USB device (the host interface of a BMC) has no PCI address. A RouterOS switch announces a bridge port as bridge/ether9; it is compared by the name of the port.
    • Add the report transceiver command #2880: values of TRANSCEIVER_INFO are 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 marker XcvrInitDone is not a port. The board serial number of bnxt_en is used. SONiC-OS- is removed from the version of show version.
    • Extend report lldp into a cabling check against NetBox #2881: both key formats of LLDP_ENTRY_TABLE are 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:

    • a node with a bond, together with the switch it is cabled to,
    • a node with an optical module, with a CMIS module and with an empty cage,
    • a node with SR-IOV VFs and/or InfiniBand ports,
    • a switch with a CMIS module,
    • community SONiC,
    • a switch where sonic-db-dump or docker exec lldp needs sudo -n.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions