diff --git a/napalm_netgear/netgear_smart.py b/napalm_netgear/netgear_smart.py index 8449e8e..38d8c30 100644 --- a/napalm_netgear/netgear_smart.py +++ b/napalm_netgear/netgear_smart.py @@ -1052,7 +1052,9 @@ class NetgearSmartDriver(ConfigLifecycleMixin, SwitchDriver): return neighbors def _get_lldp_table(self) -> List[Dict]: - """Parse ``show lldp remote-device all`` into a list of row dicts.""" + """Parse the LLDP neighbour table into a list of row dicts.""" + if self._detect_cli_v7(): + return self._get_lldp_table_v7() output = self._send_command("show lldp remote-device all") rows: List[Dict] = [] in_table = False @@ -1086,6 +1088,50 @@ class NetgearSmartDriver(ConfigLifecycleMixin, SwitchDriver): return rows + def _get_lldp_table_v7(self) -> List[Dict]: + """Parse ``show lldp neighbor`` on v7 firmware. + + The v7 CLI has neither ``show lldp remote-device all`` nor a bare + ``show lldp`` — both answer "Unknown command". Only the singular + ``neighbor`` works, and it lays the table out differently:: + + Port | Device ID | Port ID | SysName | Capabilities | TTL + ---- + ----------------- + ---------------- + ----------------- + -------------- + ----- + g9 | 10:01:02:44:37:26 |10:01:02:44:37:28 | pve-eze.eze.local | Bridge | 97 + + Ports are named ``g9`` rather than ``0/1``, and the columns are + separated by ``|`` with irregular padding — splitting on whitespace + would tear the values apart. The port ID is not always a port name; a + neighbour may identify its port by MAC. + """ + try: + output = self._send_paged_command("show lldp neighbor") + except Exception: # pragma: no cover - transport failure + return [] + + rows: List[Dict] = [] + for line in output.splitlines(): + if "|" not in line: + continue + cols = [c.strip() for c in line.split("|")] + if len(cols) < 4: + continue + port, chassis_id, remote_port, sysname = cols[0], cols[1], cols[2], cols[3] + if not port or port.lower() == "port" or set(port) <= set("-+ "): + continue + if not sysname and not remote_port: + continue + rows.append( + { + "local_port": port, + "remote_id": "", + "remote_chassis_id": chassis_id, + "port_id": remote_port, + "system_name": sysname, + } + ) + return rows + def get_lldp_neighbors_detail(self, interface: str = "") -> Dict[str, List[Dict]]: """Return detailed LLDP neighbor information. diff --git a/tests/unit/test_driver.py b/tests/unit/test_driver.py index 3b11f73..d6211ab 100644 --- a/tests/unit/test_driver.py +++ b/tests/unit/test_driver.py @@ -726,3 +726,82 @@ class TestCli: def test_raises_on_non_text_encoding(self, driver): with pytest.raises(NotImplementedError): driver.cli(["show sysinfo"], encoding="json") + + +# --------------------------------------------------------------------------- +# Tests: get_lldp_neighbors +# --------------------------------------------------------------------------- + +LLDP_NEIGHBOR_OUTPUT = """\ +show lldp neighbor + + Port | Device ID | Port ID | SysName | Capabilities | TTL + ---- + ----------------- + ---------------- + ----------------- + -------------- + ----- + g9 | 10:01:02:44:37:26 |10:01:02:44:37:28 | pve-eze.eze.local | Bridge | 97 + g10 | 10:01:02:44:37:26 |10:01:02:44:37:26 | pve-eze.eze.local | Bridge | 97 + g1 | 28:94:01:6d:26:7d |g3 | swt-core | Bridge,Router | 93 + +GS110TPv3# +""" + + +class TestGetLldpNeighbors: + """`show lldp neighbor` — the switch does speak LLDP, the driver did not. + + Without this the map has nothing to draw for the site this switch serves: + it is the core switch, so every AP and firewall behind it hangs + unconnected. Measured on a GS110TPv3 running 7.1.1.17. + """ + + def _send(self, cmd, **kw): + return LLDP_NEIGHBOR_OUTPUT if "lldp" in cmd else "" + + @staticmethod + def _as_v7(driver): + """v7 firmware — the only generation whose CLI answers `show lldp neighbor`.""" + driver._cli_v7 = True + + def test_returns_one_entry_per_local_port(self, driver): + self._as_v7(driver) + driver._send_paged_command = self._send + result = driver.get_lldp_neighbors() + assert set(result) == {"g9", "g10", "g1"} + + def test_neighbour_hostname_is_taken_from_sysname(self, driver): + self._as_v7(driver) + driver._send_paged_command = self._send + result = driver.get_lldp_neighbors() + assert result["g9"][0]["hostname"] == "pve-eze.eze.local" + + def test_remote_port_is_reported(self, driver): + self._as_v7(driver) + driver._send_paged_command = self._send + result = driver.get_lldp_neighbors() + assert result["g1"][0]["port"] == "g3" + + def test_a_mac_shaped_port_id_is_still_reported_verbatim(self, driver): + """Some neighbours identify their port by MAC rather than by name.""" + self._as_v7(driver) + driver._send_paged_command = self._send + result = driver.get_lldp_neighbors() + assert result["g9"][0]["port"] == "10:01:02:44:37:28" + + def test_header_and_prompt_lines_are_not_parsed_as_neighbours(self, driver): + self._as_v7(driver) + driver._send_paged_command = self._send + result = driver.get_lldp_neighbors() + assert "Port" not in result + assert not any(k.startswith("GS110") for k in result) + + def test_no_neighbours_yields_an_empty_mapping(self, driver): + self._as_v7(driver) + driver._send_paged_command = lambda cmd, **kw: ( + "show lldp neighbor\n\n Port | Device ID | Port ID | SysName | Capabilities | TTL\n" + " ---- + --------- + ------- + ------- + ------------ + -----\n\nGS110TPv3#\n" + ) + assert driver.get_lldp_neighbors() == {} + + def test_unsupported_firmware_does_not_raise(self, driver): + self._as_v7(driver) + driver._send_paged_command = lambda cmd, **kw: "Unknown command" + assert driver.get_lldp_neighbors() == {}