diff --git a/napalm_hpe_officeconnect/officeconnect.py b/napalm_hpe_officeconnect/officeconnect.py index a32897d..39aef12 100644 --- a/napalm_hpe_officeconnect/officeconnect.py +++ b/napalm_hpe_officeconnect/officeconnect.py @@ -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 = [] diff --git a/tests/fixtures/switching_lldp_remote.html b/tests/fixtures/switching_lldp_remote.html new file mode 100644 index 0000000..c48aa8a --- /dev/null +++ b/tests/fixtures/switching_lldp_remote.html @@ -0,0 +1,188 @@ + + + + + +
+ + +