diff --git a/backend/app/models/device.py b/backend/app/models/device.py index e5bcfbd..66fbbf0 100644 --- a/backend/app/models/device.py +++ b/backend/app/models/device.py @@ -26,7 +26,7 @@ DEVICE_ROLES = ("plate", "face", "overview", "other") DEVICE_LOCATIONS = ("server_room", "street") DEVICE_STATUSES = ("online", "offline", "unknown") -DEVICE_SOURCES = ("auto_sync", "manual", "csv_import", "sheets_sync") +DEVICE_SOURCES = ("auto_sync", "manual", "csv_import", "sheets_sync", "auto_ssh") class Device(Base, TimestampMixin): diff --git a/backend/app/services/device_discovery.py b/backend/app/services/device_discovery.py index e784c91..854eff5 100644 --- a/backend/app/services/device_discovery.py +++ b/backend/app/services/device_discovery.py @@ -155,6 +155,73 @@ async def _read_remote_config( return result.stdout +async def _discover_mikrotik_via_ssh_client( + host: str, + port: int, + username: str, + password: str, +) -> str | None: + """ + Discover MikroTik router IP via SSH_CLIENT environment variable. + When connected via L2TP → MikroTik → main_server, the SSH_CLIENT env var + shows the client IP connecting to the server (MikroTik's L2TP interface IP). + """ + try: + async with asyncssh.connect( + host, + port=port, + username=username, + password=password, + known_hosts=None, + connect_timeout=10, + ) as conn: + result = await conn.run("echo $SSH_CLIENT", check=False) + ssh_client_output = result.stdout.strip() + if ssh_client_output: + # SSH_CLIENT format: "client_ip client_port server_port" + parts = ssh_client_output.split() + if parts: + return parts[0] + except Exception as e: + print(f"[ssh_discovery] Failed to discover MikroTik via SSH_CLIENT: {e}") + return None + + +async def _discover_proxmox_via_port_scan( + host: str, + port: int, + username: str, + password: str, +) -> list[str]: + """ + Discover Proxmox servers on the local subnet by scanning port 8006. + Runs a subnet scanning script that checks port 8006 on all IPs in the subnet. + """ + try: + async with asyncssh.connect( + host, + port=port, + username=username, + password=password, + known_hosts=None, + connect_timeout=10, + ) as conn: + # Scan script: determine subnet and scan all IPs for port 8006 + scan_script = ( + "subnet=$(ip -o -f inet addr show | awk '!/ lo |docker|br-|wg/ {print $4; exit}' | " + "cut -d/ -f1 | awk -F. '{print $1\".\"$2\".\"$3}') && " + "(set +m; for ip in $subnet.{1..254}; do " + "timeout 0.3 bash -c \"echo > /dev/tcp/$ip/8006\" 2>/dev/null && echo \"$ip\" & " + "done; wait)" + ) + result = await conn.run(scan_script, check=False) + ips = [line.strip() for line in result.stdout.strip().split('\n') if line.strip()] + return ips + except Exception as e: + print(f"[ssh_discovery] Failed to discover Proxmox servers via port scan: {e}") + return [] + + def _parse_db_uri(uri: str) -> dict | None: """Parse postgresql+psycopg2://user:pass@host:port/dbname → dict.""" try: @@ -400,7 +467,9 @@ async def discover_via_ssh( result.errors.append(f"SSH to {rack_host} failed: {exc}") continue + print(f"[ssh_discovery] Processing rack {rack_host} ({rack_name})") devices_found = _parse_config(config_text, rack_host) + print(f"[ssh_discovery] Found {len(devices_found)} devices from plugins in {rack_host}") for found in devices_found: # Hostname: "rack{rack_name}-{plugin_name}", e.g. "rack01-camera_grz" @@ -427,6 +496,9 @@ async def discover_via_ssh( if existing.hostname != hostname: existing.hostname = hostname changed = True + + action = "UPDATED" if changed else "SKIPPED" + print(f"[ssh_discovery] Device {found.ip} ({hostname}) - {action}") result.updated += 1 if changed else 0 result.skipped += 0 if changed else 1 else: @@ -439,8 +511,10 @@ async def discover_via_ssh( source="auto_ssh", device_meta={"plugin": found.plugin_name, "cls": found.cls}, )) + print(f"[ssh_discovery] Device {found.ip} ({hostname}) - ADDED") result.created += 1 except Exception as exc: + print(f"[ssh_discovery] Error upserting {found.ip}: {exc}") result.errors.append(f"Error upserting {found.ip}: {exc}") # Flush after each rack so that subsequent SELECTs (for other racks @@ -452,4 +526,86 @@ async def discover_via_ssh( except Exception as exc: result.errors.append(f"Flush error after rack {rack_host}: {exc}") + # Step 3: Discover MikroTik via SSH_CLIENT + print(f"[ssh_discovery] Attempting to discover MikroTik via SSH_CLIENT on {obj.server_ip or 'main_server'}") + mikrotik_ip = await _discover_mikrotik_via_ssh_client( + host=obj.server_ip or rack_hosts[0][1], + port=obj.ssh_port or 22, + username=obj.ssh_user, + password=ssh_password, + ) + if mikrotik_ip: + print(f"[ssh_discovery] Discovered MikroTik at {mikrotik_ip}") + try: + existing = (await db.execute( + select(Device).where( + Device.object_id == obj.id, + Device.ip == mikrotik_ip, + ) + )).scalar_one_or_none() + + if existing is not None: + print(f"[ssh_discovery] Device {mikrotik_ip} (mikrotik) - SKIPPED") + result.skipped += 1 + else: + db.add(Device( + object_id=obj.id, + ip=mikrotik_ip, + hostname="mikrotik", + category="router", + role="other", + source="auto_ssh", + )) + print(f"[ssh_discovery] Device {mikrotik_ip} (mikrotik) - ADDED") + result.created += 1 + except Exception as exc: + print(f"[ssh_discovery] Error upserting {mikrotik_ip}: {exc}") + result.errors.append(f"Error upserting {mikrotik_ip}: {exc}") + + await db.flush() + else: + print(f"[ssh_discovery] No MikroTik found via SSH_CLIENT") + + # Step 4: Discover Proxmox via port 8006 scan + print(f"[ssh_discovery] Attempting to discover Proxmox servers via port 8006 scan on {obj.server_ip or 'main_server'}") + proxmox_ips = await _discover_proxmox_via_port_scan( + host=obj.server_ip or rack_hosts[0][1], + port=obj.ssh_port or 22, + username=obj.ssh_user, + password=ssh_password, + ) + if proxmox_ips: + print(f"[ssh_discovery] Discovered {len(proxmox_ips)} Proxmox server(s): {proxmox_ips}") + for idx, proxmox_ip in enumerate(proxmox_ips, 1): + hostname = f"proxmox_{idx}" + try: + existing = (await db.execute( + select(Device).where( + Device.object_id == obj.id, + Device.ip == proxmox_ip, + ) + )).scalar_one_or_none() + + if existing is not None: + print(f"[ssh_discovery] Device {proxmox_ip} ({hostname}) - SKIPPED") + result.skipped += 1 + else: + db.add(Device( + object_id=obj.id, + ip=proxmox_ip, + hostname=hostname, + category="vm", + role="other", + source="auto_ssh", + )) + print(f"[ssh_discovery] Device {proxmox_ip} ({hostname}) - ADDED") + result.created += 1 + except Exception as exc: + print(f"[ssh_discovery] Error upserting {proxmox_ip}: {exc}") + result.errors.append(f"Error upserting {proxmox_ip}: {exc}") + + await db.flush() + else: + print(f"[ssh_discovery] No Proxmox servers found via port 8006 scan") + return result