Files
lldp-mapper/scanner.py
T
dcstephenson 5ffadfae17 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
2026-07-24 10:40:55 -05:00

177 lines
6.0 KiB
Python

# scanner.py - Orchestrates the full scan pipeline
import logging
import threading
from nocodb_client import get_switches
from ssh_client import scan_all_switches
from db import (
upsert_switch, upsert_link, clear_links,
log_scan_start, log_scan_finish, merge_duplicate_switches
)
from parser import parse_neighbor_description
from exports import run_all_exports
logger = logging.getLogger(__name__)
# Global scan state (shared with Flask via import)
scan_state = {
"running": False,
"done": 0,
"total": 0,
"current_ip": None,
"current_hostname": None,
"ok": 0,
"fail": 0,
"errors": [],
"log_lines": [],
"last_scan": None,
"dept_filter": None, # None = all, "ELEC" or "GW" = dept-only
"aborted": False,
}
_abort_event = threading.Event()
def request_abort():
"""Signal the in-progress scan to stop after currently in-flight switches finish."""
if scan_state["running"]:
_abort_event.set()
return True
return False
def run_scan(dept: str = None, workers: int = 5, login_delay: int = 3):
"""
Full scan: fetch switch IPs from NocoDB → SSH all → parse → store → export.
dept: None = all active switches, "ELEC" or "GW" = dept-filtered.
"""
global scan_state
if scan_state["running"]:
logger.warning("Scan already running, skipping.")
return
_abort_event.clear()
# Fetch switch list from NocoDB (or fallback)
switches = get_switches(dept=dept)
if not switches:
logger.error("NocoDB returned no switches — aborting scan.")
scan_state["running"] = False
return
logger.error("No switches returned from NocoDB, aborting scan.")
return
scan_state.update({
"running": True,
"done": 0,
"total": len(switches),
"current_ip": None,
"current_hostname": None,
"ok": 0,
"fail": 0,
"errors": [],
"log_lines": [],
"dept_filter": dept,
"aborted": False,
})
scan_id = log_scan_start()
if dept is None:
clear_links() # Full scan only — dept scans upsert without wiping other depts
def on_progress(done, total, ip, result):
from datetime import datetime
ts = datetime.now().strftime("%H:%M:%S")
scan_state["done"] = done
scan_state["total"] = total
scan_state["current_ip"] = ip
if result["success"]:
scan_state["ok"] += 1
scan_state["current_hostname"] = result.get("hostname", ip)
vendor = result.get("vendor", "")
n = len(result.get("neighbors", []))
scan_state["log_lines"].append({
"ts": ts, "ok": True,
"text": f"{result['hostname']} ({ip}) [{vendor}] — {n} neighbor{'s' if n != 1 else ''}",
})
upsert_switch(
chassis_id=result["chassis_id"],
hostname=result["hostname"],
mgmt_ip=result["mgmt_ip"],
description=result.get("description", ""),
firmware=result.get("firmware", ""),
vendor=result.get("vendor", ""),
serial=result.get("serial", ""),
)
for neighbor in result["neighbors"]:
if neighbor.get("chassis_id") and neighbor.get("system_name"):
nbr_model, nbr_firmware = parse_neighbor_description(
neighbor.get("system_desc", "")
)
upsert_switch(
chassis_id=neighbor["chassis_id"],
hostname=neighbor["system_name"],
mgmt_ip=neighbor.get("mgmt_ip", ""),
description=nbr_model, # empty for FS neighbors; direct scan fills it in
firmware=nbr_firmware,
)
upsert_link(
chassis_a=result["chassis_id"],
port_a=neighbor["local_port"],
chassis_b=neighbor["chassis_id"],
port_b=neighbor["remote_port"],
)
else:
scan_state["fail"] += 1
error = result.get("error", "Unknown error")
scan_state["errors"].append({"ip": ip, "error": error})
error_short = error.splitlines()[0][:120]
scan_state["log_lines"].append({
"ts": ts, "ok": False,
"text": f"{ip}{error_short}",
})
scan_all_switches(switches, progress_callback=on_progress, max_workers=workers,
login_delay=login_delay, abort_event=_abort_event)
if _abort_event.is_set():
scan_state["aborted"] = True
from datetime import datetime
scan_state["log_lines"].append({
"ts": datetime.now().strftime("%H:%M:%S"), "ok": False,
"text": "⚠ Scan aborted by user",
})
try:
merge_duplicate_switches()
except Exception as e:
logger.error(f"Merge error: {e}")
try:
run_all_exports()
except Exception as e:
logger.error(f"Export error: {e}")
log_scan_finish(scan_id, scan_state["ok"], scan_state["fail"])
from datetime import datetime
scan_state["last_scan"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
scan_state["running"] = False
scan_state["current_ip"] = None
scan_state["current_hostname"] = None
scan_state["dept_filter"] = None
logger.info(f"Scan complete. OK: {scan_state['ok']}, Failed: {scan_state['fail']}")
def test_single_switch(ip: str, manufacturer: str = "", login_delay: int = 1) -> dict:
"""Test SSH connectivity/LLDP query against a single switch, independent of the main scan.
Does not touch scan_state or the database — just returns the raw connect_and_query result."""
entry = {"ip": ip, "manufacturer": manufacturer}
results = scan_all_switches([entry], max_workers=1, login_delay=login_delay)
return results[0] if results else {"success": False, "ip": ip, "error": "No result"}