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>
447 lines
16 KiB
Python
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())],
|
|
}
|