feat(lldp): report neighbours from the LLDP Remote Device Summary page

get_lldp_neighbors() was never implemented, so the switch reported no
neighbours and NetOrk's network map had nothing to draw for the site it
serves — on the reference installation two APs, a firewall and a Proxmox host
behind a 1820 8G PoE+ (J9982A) all stood unconnected.

The web UI does carry the data. /htdocs/pages/switching/lldp_remote.lsp is
served with the rows embedded in the page's aDataSet, so the existing _rows()
helper reads it with no new plumbing:

    ['7', '3', '00:...:44', '00:...:46', 'enp3s0', 'host.example.com',
     'bridge, WLAN access point, router, station only', 'bridge', '192.0.2.10']

Column 3 is the neighbour's port ID, which on many peers is a MAC, while
column 4 is its port description — a name a person can read. The description
wins, with the ID used only when it is missing. The chassis ID is passed
through as `mac` when it looks like one, since netOrk resolves a neighbour by
MAC before falling back to its name.

Fixture captured from the live switch and anonymised in the style of the
existing ones.

Closes #1
This commit is contained in:
Christian Manivong
2026-08-18 15:19:13 +07:00
parent c76a177ff2
commit cc5f615dbc
3 changed files with 256 additions and 0 deletions
+39
View File
@@ -30,6 +30,7 @@ from napalm_hpe_officeconnect.client import OfficeConnectClient
logger = logging.getLogger("napalm_hpe_officeconnect")
_MAC_RE = re.compile(r"(?:[0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}")
_SPEED_RE = re.compile(r"(\d+)\s*Mbps", re.IGNORECASE)
# Participation values that make an interface a member of a VLAN.
@@ -76,6 +77,7 @@ class OfficeConnectDriver(SwitchDriver):
PORT_STATS = "/htdocs/pages/base/port_summary_stats.lsp"
MAC_TABLE = "/htdocs/pages/base/mac_address_table.lsp"
VLAN_STATUS = "/htdocs/pages/switching/vlan_status.lsp"
LLDP_REMOTE = "/htdocs/pages/switching/lldp_remote.lsp"
VLAN_PER_PORT = "/htdocs/pages/switching/vlan_per_port.lsp?vlan={vlan}"
def __init__(
@@ -222,6 +224,43 @@ class OfficeConnectDriver(SwitchDriver):
"""Like :meth:`get_vlans` but keeping tagged and untagged apart."""
return self._vlan_participation()
def get_lldp_neighbors(self) -> dict[str, list[dict[str, str]]]:
"""Return LLDP neighbours keyed by local port.
Rows of the "LLDP Remote Device Summary" page, verbatim from a
1820 8G PoE+ (J9982A)::
['7', '3', '00:...:44', '00:...:46', 'enp3s0', 'host.example.com',
'bridge, WLAN access point, router, station only', 'bridge',
'192.0.2.10']
Column 3 is the neighbour's port *ID*, which on many peers is a MAC;
column 4 is its port *description*, which is a name a person can read.
The description is preferred and the ID only used when it is absent.
Without this the switch reports no neighbours at all, and every device
behind it — on the reference installation two APs and a firewall —
stands unconnected on the network map.
"""
neighbors: dict[str, list[dict[str, str]]] = {}
for row in self._rows(self.LLDP_REMOTE):
if len(row) < 6:
continue
local_port, chassis_id, port_id, port_desc, sysname = (
row[0],
row[2],
row[3],
row[4],
row[5],
)
if not local_port:
continue
entry = {"hostname": sysname, "port": port_desc or port_id}
if _MAC_RE.fullmatch(chassis_id or ""):
entry["mac"] = chassis_id.lower()
neighbors.setdefault(local_port, []).append(entry)
return neighbors
def get_mac_address_table(self) -> list[dict[str, Any]]:
"""Columns: [VLAN ID, MAC Address, Interface, Interface Index, Status]."""
table = []