Initial commit — LLDP network mapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dstephenson
2026-04-21 20:56:13 +00:00
commit 40d4679a59
17 changed files with 2691 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
# parser.py - Parse 'show lldp neighbors' output from FS switches
import re
def shorten_interface(iface):
"""GigabitEthernet 1/9 -> Gi1/9, TenGigabitEthernet 1/1 -> Te1/1 etc."""
replacements = [
(r'GigabitEthernet\s*', 'Gi'),
(r'TenGigabitEthernet\s*', 'Te'),
(r'TwentyFiveGigE\s*', 'Twe'),
(r'FortyGigabitEthernet\s*', 'Fo'),
(r'HundredGigE\s*', 'Hu'),
(r'FastEthernet\s*', 'Fa'),
(r'Ethernet\s*', 'Eth'),
(r'mgmt\s*', 'mgmt'),
]
for pattern, short in replacements:
iface = re.sub(pattern, short, iface, flags=re.IGNORECASE)
return iface.strip()
def parse_lldp_neighbors(raw_output, local_chassis_id, local_hostname, local_mgmt_ip):
"""
Parse raw 'show lldp neighbors' output from an FS switch.
Returns:
neighbors: list of dicts with parsed neighbor info
local_info: dict with this switch's details (enriched from LLDP data if needed)
"""
neighbors = []
# Split output into per-neighbor blocks (blank line separated)
# Each block starts with "Local Interface"
blocks = re.split(r'\n\s*\n', raw_output.strip())
for block in blocks:
if not block.strip():
continue
if 'Local Interface' not in block and 'Local Port' not in block:
continue
neighbor = {}
def extract(pattern, text, default=''):
m = re.search(pattern, text, re.IGNORECASE)
return m.group(1).strip() if m else default
neighbor['local_port'] = shorten_interface(extract(r'Local Interface\s*:\s*(.+)', block))
neighbor['chassis_id'] = extract(r'Chassis ID\s*:\s*(.+)', block)
neighbor['port_id'] = extract(r'Port ID\s*:\s*(.+)', block)
neighbor['port_desc'] = shorten_interface(extract(r'Port Description\s*:\s*(.+)', block))
neighbor['system_name'] = extract(r'System Name\s*:\s*(.+)', block)
neighbor['system_desc'] = extract(r'System Description\s*:\s*(.+)', block)
# FS switches report Management Address as MAC (e.g. '64-9D-99-AA-50-B0 (Other)')
# or as IP (e.g. '10.214.0.192'). Extract only valid IPv4.
raw_mgmt = extract(r'Management Address\s*:\s*([\d\.A-Fa-f\-:]+)', block)
ipv4_match = re.search(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})', raw_mgmt)
neighbor['mgmt_ip'] = ipv4_match.group(1) if ipv4_match else ''
neighbor['capabilities'] = extract(r'System Capabilities\s*:\s*(.+)', block)
# Only include bridge/switch neighbors (skip phones, APs listed as endpoints)
if neighbor['chassis_id'] and neighbor['system_name']:
# Use port_desc as remote port if available, fallback to port_id
neighbor['remote_port'] = neighbor['port_desc'] if neighbor['port_desc'] else neighbor['port_id']
neighbors.append(neighbor)
return neighbors
def parse_hostname_from_prompt(prompt_line):
"""Extract hostname from CLI prompt like 'ls-vhls-sw01#'"""
m = re.match(r'^([A-Za-z0-9_\-]+)[>#]', prompt_line.strip())
return m.group(1) if m else None
def parse_mgmt_ip_from_interfaces(raw_output):
"""
Parse 'show ip interface brief' to find management VLAN IP.
Looks for Vlan interfaces with an IP assigned.
Returns first Vlan IP found (typically the management VLAN).
"""
lines = raw_output.splitlines()
for line in lines:
# Match lines like: Vlan100 192.168.1.10 YES ...
m = re.match(r'\s*(Vlan\S+)\s+([\d\.]+)\s+', line, re.IGNORECASE)
if m and not m.group(2).startswith('0.0.0.0'):
return m.group(2)
return None