"""Stack health probes — VMs 112, 114, 122, 123, 130 (Spec 033 / INFRA COD).""" from __future__ import annotations import os from concurrent.futures import ThreadPoolExecutor, as_completed from datetime import datetime, timezone from typing import Any, Callable import httpx VM112_API = os.getenv("VM112_API_URL", "http://10.10.10.112:8090").rstrip("/") DESK_PUBLIC = os.getenv("DESK_PUBLIC_URL", "https://desk.ligbox.com.br").rstrip("/") API_PUBLIC = os.getenv("API_PUBLIC_URL", "https://api.ops.ligbox.com.br").rstrip("/") VM114_IP = os.getenv("VM114_IP", "10.10.10.114") VM123_IP = os.getenv("VM123_IP", "10.10.10.123") VM130_IP = os.getenv("VM130_IP", "10.10.10.130") OPENPANEL_BRIDGE = os.getenv("OPENPANEL_BRIDGE_URL", f"http://{VM123_IP}:18087").rstrip("/") OPENPANEL_BRIDGE_TOKEN = os.getenv("OPENPANEL_BRIDGE_TOKEN", "") TRAEFIK_API = os.getenv("TRAEFIK_API_URL", f"http://{VM114_IP}:8080") ProbeFn = Callable[[], dict[str, Any]] def _now() -> str: return datetime.now(timezone.utc).isoformat() def _probe_http( *, url: str, timeout: float = 8.0, verify: bool = True, headers: dict[str, str] | None = None, expect_status: tuple[int, ...] = (200), ) -> dict[str, Any]: try: with httpx.Client(timeout=timeout, verify=verify, follow_redirects=True) as client: res = client.get(url, headers=headers or {}) ok = res.status_code in expect_status return { "ok": ok, "status": "online" if ok else "check", "http_status": res.status_code, "detail": f"HTTP {res.status_code}", } except Exception as exc: return {"ok": False, "status": "down", "http_status": None, "detail": str(exc)} def _probe_redis(redis_url: str) -> dict[str, Any]: try: import redis r = redis.from_url(redis_url) r.ping() return {"ok": True, "status": "online", "detail": "PING OK"} except Exception as exc: return {"ok": False, "status": "down", "detail": str(exc)} def build_stack_catalog() -> list[dict[str, Any]]: """Catálogo estático — apps, APIs e software do stack Ligbox.""" redis_url = os.getenv("REDIS_URL", "redis://redis:6379/0") return [ { "vm": "112", "vm_label": "VM112 · Wizard & Mail", "ip": "10.10.10.112", "services": [ { "id": "vm112-onboard-api", "title": "Onboard Portal API", "spec": "001", "kind": "app", "icon": "🌐", "accent": "aqua", "url": f"{VM112_API}/api/onboarding/health", "probe": lambda: _probe_http(url=f"{VM112_API}/api/onboarding/health"), }, { "id": "vm112-onboard-ui", "title": "Onboard Wizard UI", "spec": "025", "kind": "app", "icon": "🧭", "accent": "aqua", "url": "https://onboard.ligbox.com.br", "probe": lambda: _probe_http( url="https://onboard.ligbox.com.br/", expect_status=(200, 301, 302, 403), ), }, { "id": "vm112-carbonio", "title": "Carbonio EmailServer", "spec": "022", "kind": "sw", "icon": "✉️", "accent": "slate", "url": f"https://{VM112_IP}:443", "probe": lambda: _probe_http( url=f"https://{VM112_IP}/", verify=False, expect_status=(200, 301, 302, 403, 404), ), }, { "id": "vm112-domain-api", "title": "Domain Admin API", "spec": "017", "kind": "api", "icon": "🗂️", "accent": "aqua", "url": f"{VM112_API}/api/admin/domains", "probe": lambda: _probe_http( url=f"{VM112_API}/api/onboarding/health", expect_status=(200), ), }, ], }, { "vm": "114", "vm_label": "CT114 · Edge Traefik", "ip": VM114_IP, "services": [ { "id": "vm114-traefik", "title": "Traefik API", "spec": "026", "kind": "sw", "icon": "🚦", "accent": "slate", "url": f"{TRAEFIK_API}/api/overview", "probe": lambda: _probe_http( url=f"{TRAEFIK_API}/api/overview", expect_status=(200, 401, 403), ), }, { "id": "vm114-desk-route", "title": "Router Desk (WAN)", "spec": "026", "kind": "integration", "icon": "🔗", "accent": "teal", "url": DESK_PUBLIC, "probe": lambda: _probe_http( url=f"{DESK_PUBLIC}/", expect_status=(200, 301, 302), ), }, { "id": "vm114-api-route", "title": "Router API Ops (WAN)", "spec": "027", "kind": "integration", "icon": "⚙️", "accent": "teal", "url": f"{API_PUBLIC}/health", "probe": lambda: _probe_http(url=f"{API_PUBLIC}/health"), }, ], }, { "vm": "122", "vm_label": "VM122 · Ops Desk", "ip": "10.10.10.122", "services": [ { "id": "vm122-desk-api", "title": "Desk API (FastAPI)", "spec": "003", "kind": "app", "icon": "⚙️", "accent": "teal", "url": "http://127.0.0.1:8080/health", "probe": lambda: _probe_http(url="http://127.0.0.1:8080/health"), }, { "id": "vm122-desk-ui", "title": "Desk Frontend", "spec": "033", "kind": "app", "icon": "🖥️", "accent": "teal", "url": "http://10.10.10.122:8091", "probe": lambda: _probe_http( url="http://10.10.10.122:8091/", expect_status=(200, 301, 302), ), }, { "id": "vm122-redis", "title": "Redis", "spec": "—", "kind": "sw", "icon": "🧠", "accent": "slate", "url": redis_url, "probe": lambda: _probe_redis(redis_url), }, { "id": "vm122-webhook-soc", "title": "Webhook SOC VM112", "spec": "001", "kind": "integration", "icon": "📡", "accent": "teal", "url": "/api/v1/integrations/health", "probe": lambda: {"ok": True, "status": "online", "detail": "via integrations/health"}, }, { "id": "vm122-purge-auth", "title": "API Purge Auth", "spec": "032", "kind": "api", "icon": "🔐", "accent": "rose", "url": "/api/v1/infra/purge-auth-domains", "probe": lambda: {"ok": True, "status": "online", "detail": "módulo local"}, }, ], }, { "vm": "123", "vm_label": "VM123 · Finance & Hosting", "ip": VM123_IP, "services": [ { "id": "vm123-foss", "title": "FOSSBilling", "spec": "024", "kind": "app", "icon": "💰", "accent": "orange", "url": f"http://{VM123_IP}:8092", "probe": lambda: _probe_http( url=f"http://{VM123_IP}:8092/", expect_status=(200, 301, 302, 403), ), }, { "id": "vm123-odoo", "title": "Odoo 16", "spec": "024", "kind": "app", "icon": "📊", "accent": "violet", "url": f"http://{VM123_IP}:8069", "probe": lambda: _probe_http( url=f"http://{VM123_IP}:8069/web/login", expect_status=(200, 301, 302), ), }, { "id": "vm123-openpanel-ui", "title": "OpenPanel CE", "spec": "028", "kind": "app", "icon": "🎛️", "accent": "orange", "url": "https://openpanel.ligbox.com.br", "probe": lambda: _probe_http( url="https://openpanel.ligbox.com.br/", expect_status=(200, 301, 302, 403), ), }, { "id": "vm123-openadmin", "title": "OpenAdmin", "spec": "024", "kind": "app", "icon": "🔧", "accent": "orange", "url": "https://admin.openpanel.ligbox.com.br", "probe": lambda: _probe_http( url="https://admin.openpanel.ligbox.com.br/", expect_status=(200, 301, 302, 403), ), }, { "id": "vm123-openpanel-bridge", "title": "OpenPanel FOSS Bridge", "spec": "028", "kind": "api", "icon": "🌉", "accent": "orange", "url": f"{OPENPANEL_BRIDGE}/api", "probe": _probe_openpanel_bridge, }, { "id": "vm123-ops-console", "title": "Ops Console UI", "spec": "019", "kind": "app", "icon": "🖥️", "accent": "aqua", "url": f"http://{VM123_IP}:8100/health", "probe": lambda: _probe_http(url=f"http://{VM123_IP}:8100/health"), }, { "id": "vm123-phpmyadmin", "title": "phpMyAdmin", "spec": "—", "kind": "sw", "icon": "🗄️", "accent": "slate", "url": f"http://{VM123_IP}:8888", "probe": lambda: _probe_http( url=f"http://{VM123_IP}:8888/", expect_status=(200, 301, 302, 403), ), }, { "id": "vm123-ollama", "title": "Ollama LLM", "spec": "029", "kind": "api", "icon": "🤖", "accent": "violet", "url": f"http://{VM123_IP}:11434/api/tags", "probe": lambda: _probe_http(url=f"http://{VM123_IP}:11434/api/tags"), }, ], }, { "vm": "130", "vm_label": "CT130 · Spec Hub", "ip": VM130_IP, "services": [ { "id": "vm130-forgejo", "title": "Forgejo Git", "spec": "031", "kind": "app", "icon": "🦊", "accent": "violet", "url": f"http://{VM130_IP}:3000", "probe": lambda: _probe_http( url=f"http://{VM130_IP}:3000/", expect_status=(200, 301, 302, 403), ), }, { "id": "vm130-spec-portal", "title": "Spec Portal", "spec": "031", "kind": "app", "icon": "📚", "accent": "aqua", "url": f"http://{VM130_IP}:8080", "probe": lambda: _probe_http(url=f"http://{VM130_IP}:8080/"), }, { "id": "vm130-spec-public", "title": "Spec Hub (WAN)", "spec": "031", "kind": "integration", "icon": "🔗", "accent": "violet", "url": "https://spec.ligbox.com.br", "probe": lambda: _probe_http( url="https://spec.ligbox.com.br/", expect_status=(200, 301, 302), ), }, ], }, ] def _probe_openpanel_bridge() -> dict[str, Any]: if not OPENPANEL_BRIDGE_TOKEN: return {"ok": False, "status": "check", "detail": "OPENPANEL_BRIDGE_TOKEN ausente"} try: with httpx.Client(timeout=10.0) as client: res = client.get( f"{OPENPANEL_BRIDGE}/api", headers={"Authorization": f"Bearer {OPENPANEL_BRIDGE_TOKEN}"}, ) ok = res.status_code < 400 return {"ok": ok, "status": "online" if ok else "check", "http_status": res.status_code, "detail": f"HTTP {res.status_code}"} except Exception as exc: return {"ok": False, "status": "down", "detail": str(exc)} def run_stack_health() -> dict[str, Any]: catalog = build_stack_catalog() vm_out: list[dict[str, Any]] = [] total = ok_count = 0 def run_one(vm_block: dict[str, Any], svc: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]: probe = svc.get("probe") result = probe() if callable(probe) else {"ok": False, "status": "unknown", "detail": "sem probe"} row = { "id": svc["id"], "title": svc["title"], "spec": svc["spec"], "kind": svc["kind"], "icon": svc["icon"], "accent": svc["accent"], "url": svc["url"], "ok": bool(result.get("ok")), "status": result.get("status") or ("online" if result.get("ok") else "down"), "http_status": result.get("http_status"), "detail": result.get("detail") or "", } return vm_block, row futures = [] with ThreadPoolExecutor(max_workers=12) as pool: for vm_block in catalog: for svc in vm_block["services"]: futures.append(pool.submit(run_one, vm_block, svc)) vm_map: dict[str, dict[str, Any]] = {} for vm_block in catalog: vm_map[vm_block["vm"]] = { "vm": vm_block["vm"], "vm_label": vm_block["vm_label"], "ip": vm_block["ip"], "services": [], } for fut in as_completed(futures): vm_block, row = fut.result() vm_map[vm_block["vm"]]["services"].append(row) total += 1 if row["ok"]: ok_count += 1 for vm_id in vm_map: vm_map[vm_id]["services"].sort(key=lambda s: s["title"]) return { "generated_at": _now(), "summary": { "total": total, "ok": ok_count, "degraded": total - ok_count, "vms": list(vm_map.keys()), }, "vms": [vm_map[k] for k in sorted(vm_map.keys())], }