# 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"}