Add SEC/AMI scans, scan abort, connection test, runtime credentials, serial tracking
Also fixes several LLDP parsing edge cases (blank-field regex bleed, endpoint-device filtering, HP ProCurve chassis ID format, Aruba port naming), corrects link dedup to prefer self-reported port names over neighbor guesses, and repoints NetBox sync at the new instance where devices are hand-curated rather than auto-created. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bp1HutYUh9fmTFgwB4qBLz
This commit is contained in:
@@ -10,21 +10,10 @@ def normalize_mac(mac_str):
|
||||
return '-'.join(clean[i:i+2] for i in range(0, 12, 2))
|
||||
|
||||
|
||||
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 port_number(iface):
|
||||
"""Reduce any interface label to its trailing port number: 'GigabitEthernet1/9' -> '9', 'E14' -> '14', '9' -> '9'."""
|
||||
m = re.search(r'(\d+)$', (iface or '').strip())
|
||||
return m.group(1) if m else (iface or '').strip()
|
||||
|
||||
|
||||
def parse_lldp_neighbors(raw_output, local_chassis_id, local_hostname, local_mgmt_ip):
|
||||
@@ -53,23 +42,30 @@ def parse_lldp_neighbors(raw_output, local_chassis_id, local_hostname, local_mgm
|
||||
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)
|
||||
# [ \t]* (not \s*) after the colon — a blank field followed immediately by
|
||||
# the next field line must not let the match bleed across the newline into it.
|
||||
neighbor['local_port'] = port_number(extract(r'Local Interface\s*:[ \t]*(.+)', block))
|
||||
neighbor['chassis_id'] = extract(r'Chassis ID\s*:[ \t]*(.+)', block)
|
||||
neighbor['port_id'] = extract(r'Port ID\s*:[ \t]*(.+)', block)
|
||||
neighbor['port_desc'] = port_number(extract(r'Port Description\s*:[ \t]*(.+)', block))
|
||||
neighbor['system_name'] = extract(r'System Name\s*:[ \t]*(.+)', block)
|
||||
neighbor['system_desc'] = extract(r'System Description\s*:[ \t]*(.+)', 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)
|
||||
neighbor['capabilities'] = extract(r'System Capabilities\s*:[ \t]*(.+)', 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']
|
||||
# Only include bridge/router neighbors — skip phones, APs, print servers,
|
||||
# cameras, etc. that show up as "Station Only" or similar endpoint-only capabilities.
|
||||
is_bridge_or_router = re.search(r'Bridge|Router', neighbor['capabilities'], re.IGNORECASE)
|
||||
if neighbor['chassis_id'] and neighbor['system_name'] and is_bridge_or_router:
|
||||
# Port ID is the mandatory, standardized LLDP field — prefer it. Port
|
||||
# Description is optional free text; some vendors (e.g. Dell OS10) put
|
||||
# human documentation there ("SUB-FAIR-SW01 9") that isn't a real port
|
||||
# name and can collide across interfaces, so it's only a fallback.
|
||||
neighbor['remote_port'] = port_number(neighbor['port_id']) if neighbor['port_id'] else neighbor['port_desc']
|
||||
neighbors.append(neighbor)
|
||||
|
||||
return neighbors
|
||||
@@ -100,6 +96,14 @@ def parse_aruba_procurve_local(raw_output):
|
||||
'system_desc': system_desc, 'mgmt_ip': mgmt_ip}
|
||||
|
||||
|
||||
|
||||
# Older HP ProCurve firmware (e.g. 5412zl / J8698A) prints ChassisId as six
|
||||
# space-separated hex octets ('64 9d 99 b8 f8 d0') instead of the single
|
||||
# colon/dash-joined token newer Aruba switches use — match that form first,
|
||||
# since a naive whitespace split misreads the octets as separate columns.
|
||||
_SPACED_MAC_RE = re.compile(r'^((?:[0-9A-Fa-f]{2}\s+){5}[0-9A-Fa-f]{2})\s+(.*)$')
|
||||
|
||||
|
||||
def parse_aruba_procurve_neighbors(raw_output):
|
||||
"""
|
||||
Parse 'show lldp info remote-device' tabular output from HP/Aruba switch.
|
||||
@@ -116,14 +120,26 @@ def parse_aruba_procurve_neighbors(raw_output):
|
||||
continue
|
||||
|
||||
left, right = line.split('|', 1)
|
||||
# Not passed through port_number(): Aruba port names encode the module/slot
|
||||
# (e.g. 'E14', 'F24') which is meaningful and must not be reduced to bare digits.
|
||||
local_port = left.strip()
|
||||
parts = right.split()
|
||||
if len(parts) < 4:
|
||||
continue
|
||||
right = right.strip()
|
||||
|
||||
chassis_id = normalize_mac(parts[0])
|
||||
port_id = parts[1]
|
||||
sys_name = parts[3]
|
||||
m = _SPACED_MAC_RE.match(right)
|
||||
if m:
|
||||
chassis_id = normalize_mac(m.group(1))
|
||||
rest = m.group(2).split()
|
||||
else:
|
||||
parts = right.split()
|
||||
if len(parts) < 4:
|
||||
continue
|
||||
chassis_id = normalize_mac(parts[0])
|
||||
rest = parts[1:]
|
||||
|
||||
if len(rest) < 3:
|
||||
continue
|
||||
port_id = rest[0]
|
||||
sys_name = rest[-1]
|
||||
|
||||
neighbors.append({
|
||||
'local_port': local_port,
|
||||
|
||||
Reference in New Issue
Block a user