feat: Phase 3 & 4 — monitoring, discovery, polish, deployment
Phase 3 — Discovery & Monitoring: - Network scanner: nmap wrapper + mock fallback, fingerprint service (35 signatures) - Status checker: ping/http/https/tcp/ssh/prometheus/health per-node checks - APScheduler: status checks every 60s, WebSocket broadcast - WebSocket /ws/status: live node status updates to frontend - Sidebar panels: Pending Devices, Hidden Devices, Scan History - Auth token persisted to localStorage (survive page refresh) - 24 new backend tests (scan flow + status_checker) Phase 4 — Polish & Deployment: - Auto-layout: Dagre hierarchical TB via Toolbar button - Export PNG: html-to-image download via Toolbar button - Scan config modal: CIDR ranges + check interval, GET/POST /api/v1/scan/config - Dockerfile.backend (Python 3.13 slim + nmap), Dockerfile.frontend (nginx) - docker-compose.yml with data volume and NET_RAW cap for ping - scripts/lxc-install.sh: Proxmox VE systemd bootstrap - README.md: quick-start, config reference, stack overview
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
"""Per-node status checks: ping, http, https, tcp, ssh, prometheus, health."""
|
||||
import asyncio
|
||||
import logging
|
||||
import socket
|
||||
import time
|
||||
|
||||
import httpx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def check_node(check_method: str, target: str | None, ip: str | None) -> dict:
|
||||
"""
|
||||
Run the appropriate check and return {status, response_time_ms}.
|
||||
status is one of: online, offline, unknown.
|
||||
"""
|
||||
host = target or ip
|
||||
if not host:
|
||||
return {"status": "unknown", "response_time_ms": None}
|
||||
|
||||
start = time.monotonic()
|
||||
try:
|
||||
match check_method:
|
||||
case "ping":
|
||||
ok = await _ping(host)
|
||||
case "http":
|
||||
url = host if host.startswith("http") else f"http://{host}"
|
||||
ok = await _http_get(url)
|
||||
case "https":
|
||||
url = host if host.startswith("https") else f"https://{host}"
|
||||
ok = await _http_get(url, verify=True)
|
||||
case "tcp":
|
||||
host_part, _, port_str = host.rpartition(":")
|
||||
port = int(port_str) if port_str.isdigit() else 80
|
||||
ok = await _tcp_connect(host_part or host, port)
|
||||
case "ssh":
|
||||
ok = await _tcp_connect(host, 22)
|
||||
case "prometheus":
|
||||
url = host if host.startswith("http") else f"http://{host}/metrics"
|
||||
ok = await _http_get(url)
|
||||
case "health":
|
||||
url = host if host.startswith("http") else f"http://{host}/health"
|
||||
ok = await _http_get(url)
|
||||
case _:
|
||||
ok = await _ping(host)
|
||||
|
||||
elapsed_ms = int((time.monotonic() - start) * 1000)
|
||||
return {"status": "online" if ok else "offline", "response_time_ms": elapsed_ms}
|
||||
|
||||
except Exception as exc:
|
||||
logger.debug("Check failed for %s (%s): %s", host, check_method, exc)
|
||||
return {"status": "offline", "response_time_ms": None}
|
||||
|
||||
|
||||
async def _ping(host: str) -> bool:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"ping", "-c", "1", "-W", "1", host,
|
||||
stdout=asyncio.subprocess.DEVNULL,
|
||||
stderr=asyncio.subprocess.DEVNULL,
|
||||
)
|
||||
await proc.wait()
|
||||
return proc.returncode == 0
|
||||
|
||||
|
||||
async def _http_get(url: str, verify: bool = False) -> bool:
|
||||
async with httpx.AsyncClient(verify=verify, timeout=5) as client:
|
||||
resp = await client.get(url, follow_redirects=True)
|
||||
return resp.status_code < 500
|
||||
|
||||
|
||||
async def _tcp_connect(host: str, port: int) -> bool:
|
||||
try:
|
||||
_, writer = await asyncio.wait_for(
|
||||
asyncio.open_connection(host, port), timeout=3
|
||||
)
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
return True
|
||||
except (TimeoutError, OSError, socket.gaierror):
|
||||
return False
|
||||
Reference in New Issue
Block a user