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:
+44
-12
@@ -7,7 +7,8 @@ import socket
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from functools import partial
|
||||
from netmiko import ConnectHandler, NetmikoTimeoutException, NetmikoAuthenticationException
|
||||
from config import SSH_USERNAME, SSH_PASSWORD, SSH_PORT, SSH_TIMEOUT, DEVICE_TYPE
|
||||
from config import SSH_PORT, SSH_TIMEOUT, DEVICE_TYPE
|
||||
import db
|
||||
from parser import (parse_lldp_neighbors, parse_mgmt_ip_from_interfaces,
|
||||
parse_aruba_procurve_local, parse_aruba_procurve_neighbors,
|
||||
normalize_mac)
|
||||
@@ -35,12 +36,18 @@ def _device_type_for(manufacturer: str) -> str:
|
||||
return DEVICE_TYPE
|
||||
|
||||
|
||||
def _get_credentials():
|
||||
"""SSH credentials, editable at runtime via /api/credentials (stored in the settings table)."""
|
||||
return db.get_setting("ssh_username"), db.get_setting("ssh_password")
|
||||
|
||||
|
||||
def connect_and_query(ip, login_delay=3, device_type=None):
|
||||
username, password = _get_credentials()
|
||||
device = {
|
||||
"device_type": device_type or DEVICE_TYPE,
|
||||
"host": ip,
|
||||
"username": SSH_USERNAME,
|
||||
"password": SSH_PASSWORD,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"port": SSH_PORT,
|
||||
"timeout": SSH_TIMEOUT,
|
||||
"global_delay_factor": 2,
|
||||
@@ -78,9 +85,9 @@ def connect_and_query(ip, login_delay=3, device_type=None):
|
||||
vendor = _detect_vendor(version_output)
|
||||
|
||||
if vendor == 'aruba_procurve':
|
||||
chassis_id, mgmt_ip, model, firmware, neighbors = _query_aruba(conn, version_output, ip)
|
||||
chassis_id, mgmt_ip, model, firmware, serial, neighbors = _query_aruba(conn, version_output, ip)
|
||||
else:
|
||||
chassis_id, mgmt_ip, model, firmware, neighbors = _query_fs(conn, version_output, ip)
|
||||
chassis_id, mgmt_ip, model, firmware, serial, neighbors = _query_fs(conn, version_output, ip)
|
||||
finally:
|
||||
conn.disconnect()
|
||||
|
||||
@@ -94,6 +101,7 @@ def connect_and_query(ip, login_delay=3, device_type=None):
|
||||
"mgmt_ip": mgmt_ip,
|
||||
"description": model,
|
||||
"firmware": firmware,
|
||||
"serial": serial,
|
||||
"vendor": vendor,
|
||||
"neighbors": neighbors,
|
||||
}
|
||||
@@ -110,7 +118,7 @@ def connect_and_query(ip, login_delay=3, device_type=None):
|
||||
|
||||
|
||||
def _query_fs(conn, version_output, ip):
|
||||
"""Run FS/IES switch-specific commands and return (chassis_id, mgmt_ip, model, firmware, neighbors)."""
|
||||
"""Run FS/IES switch-specific commands and return (chassis_id, mgmt_ip, model, firmware, serial, neighbors)."""
|
||||
intf_output = conn.send_command("show ip interface brief", read_timeout=30)
|
||||
local_info_output = conn.send_command("show lldp local-information", read_timeout=30)
|
||||
lldp_output = conn.send_command("show lldp neighbors", read_timeout=30)
|
||||
@@ -123,24 +131,27 @@ def _query_fs(conn, version_output, ip):
|
||||
|
||||
model = _fs_model(version_output) or _extract_system_desc(local_info_output)
|
||||
firmware = _fs_firmware(version_output)
|
||||
serial = _extract_serial(version_output)
|
||||
neighbors = parse_lldp_neighbors(lldp_output, chassis_id, '', mgmt_ip)
|
||||
|
||||
return chassis_id, mgmt_ip, model, firmware, neighbors
|
||||
return chassis_id, mgmt_ip, model, firmware, serial, neighbors
|
||||
|
||||
|
||||
def _query_aruba(conn, version_output, ip):
|
||||
"""Run HP/Aruba ProCurve-specific commands and return (chassis_id, mgmt_ip, model, firmware, neighbors)."""
|
||||
"""Run HP/Aruba ProCurve-specific commands and return (chassis_id, mgmt_ip, model, firmware, serial, neighbors)."""
|
||||
local_output = conn.send_command("show lldp info local-device", read_timeout=30)
|
||||
remote_output = conn.send_command("show lldp info remote-device", read_timeout=30)
|
||||
sysinfo_output = conn.send_command("show system information", read_timeout=30)
|
||||
|
||||
local_info = parse_aruba_procurve_local(local_output)
|
||||
chassis_id = local_info['chassis_id'] or _extract_mac_from_version(version_output) or ip
|
||||
mgmt_ip = local_info['mgmt_ip'] or ip
|
||||
model = _aruba_model(local_info.get('system_desc', ''))
|
||||
firmware = _aruba_firmware(version_output)
|
||||
serial = _extract_serial(sysinfo_output) or _extract_serial(version_output)
|
||||
neighbors = parse_aruba_procurve_neighbors(remote_output)
|
||||
|
||||
return chassis_id, mgmt_ip, model, firmware, neighbors
|
||||
return chassis_id, mgmt_ip, model, firmware, serial, neighbors
|
||||
|
||||
|
||||
# ── Vendor detection ──────────────────────────────────────────────────────────
|
||||
@@ -165,6 +176,16 @@ def _extract_mac_from_version(output):
|
||||
return m.group(1).strip() if m else None
|
||||
|
||||
|
||||
def _extract_serial(output):
|
||||
"""Serial number field — covers 'Serial Number:'/'Serial No:'/'Serial:' spellings
|
||||
plus FS/IES's 'SN :' field (confirmed from a live rec-10016-sw01 'show version')."""
|
||||
m = re.search(r'Serial\s*(?:No\.?|Number)?\s*:\s*(\S+)', output, re.IGNORECASE)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
m = re.search(r'^\s*SN\s*:\s*(\S+)', output, re.IGNORECASE | re.MULTILINE)
|
||||
return m.group(1).strip() if m else ''
|
||||
|
||||
|
||||
def _fs_model(output):
|
||||
m = re.search(r'(?:Model Name|Product)\s*:\s*([\w\-]+)', output, re.IGNORECASE)
|
||||
return f"FS {m.group(1)}" if m else ''
|
||||
@@ -203,8 +224,10 @@ def _aruba_firmware(version_output):
|
||||
|
||||
# ── Scan orchestration ────────────────────────────────────────────────────────
|
||||
|
||||
def scan_all_switches(switch_list, progress_callback=None, max_workers=5, login_delay=3):
|
||||
"""switch_list: list of IP strings or dicts with 'ip' and optional 'manufacturer'."""
|
||||
def scan_all_switches(switch_list, progress_callback=None, max_workers=5, login_delay=3, abort_event=None):
|
||||
"""switch_list: list of IP strings or dicts with 'ip' and optional 'manufacturer'.
|
||||
abort_event: optional threading.Event — when set, stops queueing new work and
|
||||
returns without waiting on switches still in flight."""
|
||||
results = []
|
||||
total = len(switch_list)
|
||||
done = 0
|
||||
@@ -217,7 +240,8 @@ def scan_all_switches(switch_list, progress_callback=None, max_workers=5, login_
|
||||
ip, dt = entry, None
|
||||
return ip, executor.submit(connect_and_query, ip, login_delay=login_delay, device_type=dt)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
executor = ThreadPoolExecutor(max_workers=max_workers)
|
||||
try:
|
||||
future_to_ip = {}
|
||||
for entry in switch_list:
|
||||
ip, fut = _submit(entry)
|
||||
@@ -237,4 +261,12 @@ def scan_all_switches(switch_list, progress_callback=None, max_workers=5, login_
|
||||
if progress_callback:
|
||||
progress_callback(done, total, ip, result)
|
||||
|
||||
if abort_event is not None and abort_event.is_set():
|
||||
logger.warning("Scan aborted — stopping after in-flight switches finish.")
|
||||
break
|
||||
finally:
|
||||
# wait=False: don't block on switches still logging in; they'll finish
|
||||
# in the background and their results are discarded.
|
||||
executor.shutdown(wait=False, cancel_futures=True)
|
||||
|
||||
return results
|
||||
|
||||
Reference in New Issue
Block a user