ligbox-ops-platform/projects/ops-desk/api/app/stack_health.py
Ligbox Spec Hub 50085b7d94 Fix stack status 500 (VM112_IP) and rename INFRA COD to INFRA CODE.
Define missing VM112_IP in stack_health probes; update menu and page titles to Infrastructure as Code branding.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-19 22:43:43 +00:00

447 lines
16 KiB
Python

"""Stack health probes — VMs 112, 114, 122, 123, 130 (Spec 033 / INFRA CODE)."""
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_IP = os.getenv("VM112_IP", "10.10.10.112")
VM112_API = os.getenv("VM112_API_URL", f"http://{VM112_IP}: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())],
}