# Copyright 2025 The NetOrk Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """LLDP-related NAPALM getters for Proxmox VE nodes.""" from __future__ import annotations import re from typing import Any _JsonDict = dict[str, Any] class ProxmoxLLDPMixin: """Mixin providing LLDP NAPALM methods.""" def get_lldp_neighbors(self) -> dict[str, list[_JsonDict]]: """Return LLDP neighbours (requires lldpd on the Proxmox node).""" result: dict[str, list[_JsonDict]] = {} raw = self._exec_ssh_command( "lldpcli show neighbors summary 2>/dev/null || true" ) current_iface = "" for line in raw.splitlines(): m_iface = re.match(r"^\s*Interface:\s+(\S+?),?\s", line) if m_iface: current_iface = m_iface.group(1) result.setdefault(current_iface, []) continue m_sys = re.match(r"^\s*SysName:\s+(.+)", line) m_port = re.match(r"^\s*PortID:\s+\S+\s+(.+)", line) if m_sys and current_iface: hostname = m_sys.group(1).strip() if result[current_iface]: result[current_iface][-1]["hostname"] = hostname else: result[current_iface].append({"hostname": hostname, "port": ""}) if m_port and current_iface and result[current_iface]: result[current_iface][-1]["port"] = m_port.group(1).strip() return result def get_lldp_neighbors_detail(self, interface: str = "") -> dict[str, list[_JsonDict]]: """Return detailed LLDP neighbour information.""" result: dict[str, list[_JsonDict]] = {} raw = self._exec_ssh_command( "lldpcli show neighbors details 2>/dev/null || true" ) current_iface = "" current_entry: _JsonDict = {} def _flush(): if current_iface and current_entry: result.setdefault(current_iface, []).append(current_entry.copy()) for line in raw.splitlines(): m_iface = re.match(r"^\s*Interface:\s+(\S+?),?\s", line) if m_iface: _flush() current_iface = m_iface.group(1) if interface and current_iface != interface: current_iface = "" current_entry = { "parent_interface": "", "remote_chassis_id": "", "remote_system_name": "", "remote_port": "", "remote_port_description": "", "remote_system_description": "", "remote_system_capab": [], "remote_system_enable_capab": [], } continue if not current_iface: continue for key, pattern in ( ("remote_chassis_id", r"ChassisID:\s+\S+\s+(.+)"), ("remote_system_name", r"SysName:\s+(.+)"), ("remote_port", r"PortID:\s+\S+\s+(.+)"), ("remote_port_description", r"PortDescr:\s+(.+)"), ("remote_system_description", r"SysDescr:\s+(.+)"), ): m = re.match(rf"^\s*{pattern}", line) if m: current_entry[key] = m.group(1).strip() m_cap = re.match(r"^\s*Capability:\s+(\S+),\s+(\w+)", line) if m_cap: cap = m_cap.group(1).lower() enabled = m_cap.group(2).lower() == "on" current_entry["remote_system_capab"].append(cap) if enabled: current_entry["remote_system_enable_capab"].append(cap) _flush() return result