Files
atc-agents/api/workload.py
T
root b46e7f01dd Fix Dockhand auth for topology flow and accurate CDC change stats.
Pass DOCKHAND_API_TOKEN to container inventory calls so pipeline_active
and topology animation work after Authentik. Replace ring-buffer-only CDC
stats with minute rollups (no 1000 cap), add 15m/1h/6h/24h window selector
on the Live Changes tab, and poll recent events on an interval.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 15:15:15 +02:00

325 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Build UI workload + topology payload from lab snapshot."""
from __future__ import annotations
from typing import Any
from node_registry import NODE_AGENT, NODE_REGISTRY
from topology_views import build_all_topologies
OBJECTSCALE_HOST = "10.0.20.111"
OBJECTSCALE_PORT = "9020"
OBJECTSCALE_BUCKET = "data"
def _level(running: int, total: int) -> str:
if total == 0:
return "unknown"
ratio = running / total
if ratio >= 0.9:
return "ok"
if ratio >= 0.5:
return "warn"
return "down"
def _app_row(c: dict[str, Any]) -> dict[str, Any]:
img = c.get("image") or ""
short_img = img.split("/")[-1].split(":")[0][:20]
return {
"name": c.get("name", "?"),
"state": c.get("state", "unknown"),
"image": short_img,
"ports": c.get("ports") or [],
"host": c.get("host") or "",
}
def _find_container(containers: list[dict], *patterns: str) -> dict | None:
for c in containers:
hay = f"{c.get('name', '')} {c.get('image', '')}".lower()
if any(p.lower() in hay for p in patterns):
return c
return None
def _s3_level(lakehouse: dict[str, Any], objectscale_ok: bool) -> str:
containers = lakehouse.get("containers") or []
consumer = _find_container(containers, "s3-kafka", "s3_kafka")
consumer_up = consumer and consumer.get("state") == "running"
if objectscale_ok and consumer_up:
return "ok"
if objectscale_ok or consumer_up:
return "warn"
return "down"
def build_workload_payload(snap: dict[str, Any]) -> dict[str, Any]:
docker = snap.get("docker", {})
databases = snap.get("databases", {})
lakehouse = snap.get("lakehouse", {})
etl = snap.get("etl", {})
hadoop = snap.get("hadoop", {})
gpu = snap.get("gpu", {})
objectscale = snap.get("objectscale", {})
command = snap.get("command_center", {})
docker_apps = [_app_row(c) for c in docker.get("containers", [])]
db_apps = [_app_row(c) for c in databases.get("containers", [])]
lake_apps = [_app_row(c) for c in lakehouse.get("containers", [])]
lake_containers = lakehouse.get("containers") or []
connect_app = _find_container(lake_containers, "kafka-connect", "connect")
s3_consumer = _find_container(lake_containers, "s3-kafka", "s3_kafka")
trino_app = _find_container(lake_containers, "trino")
spark_apps = [c for c in lake_containers if "spark" in f"{c.get('name', '')} {c.get('image', '')}".lower()]
hdfs_ok = hadoop.get("reachable", False)
etl_ok = etl.get("airflow_healthy") and etl.get("kafka_ui_ok")
objectscale_ok = objectscale.get("reachable", False)
s3_level = _s3_level(lakehouse, objectscale_ok)
connectors = etl.get("connectors") or []
etl_apps = [
{"name": "Airflow", "state": "running" if etl.get("airflow_healthy") else "down", "image": "airflow", "ports": ["8080"], "host": "10.0.21.55"},
{"name": "Kafka", "state": "running" if etl.get("kafka_ui_ok") else "down", "image": "kafka", "ports": ["9092"], "host": "10.0.21.36"},
{"name": "Kafka UI", "state": "running" if etl.get("kafka_ui_ok") else "down", "image": "kafka-ui", "ports": ["9000"], "host": "10.0.21.36"},
*[
{"name": c, "state": "running", "image": "connect", "ports": ["8083"], "host": lakehouse.get("host", "10.0.21.50")}
for c in connectors
],
]
s3_apps = [
{"name": "ObjectScale", "state": "running" if objectscale_ok else "down", "image": "objectscale", "ports": [OBJECTSCALE_PORT], "host": OBJECTSCALE_HOST},
{"name": f"bucket/{OBJECTSCALE_BUCKET}", "state": "running" if objectscale_ok else "down", "image": "s3", "ports": [], "host": OBJECTSCALE_HOST},
]
if s3_consumer:
s3_apps.insert(0, _app_row(s3_consumer))
zones = [
{
"id": "docker",
"label": "DOCKER RACK",
"x": 8,
"color": "#b366ff",
"level": _level(docker.get("running", 0), docker.get("total", 1) or 1),
"running": docker.get("running", 0),
"total": docker.get("total", 0),
"apps": docker_apps,
"vm": "atc-docker01",
"ip": "10.0.21.45",
},
{
"id": "db",
"label": "DB VAULT",
"x": 22,
"color": "#ffaa00",
"level": _level(databases.get("running", 0), databases.get("total", 1) or 1),
"running": databases.get("running", 0),
"total": databases.get("total", 0),
"apps": db_apps,
"vm": "atc-db02",
"ip": "10.0.21.51",
},
{
"id": "etl",
"label": "ETL PIPE",
"x": 38,
"color": "#00f0ff",
"level": "ok" if etl_ok else "warn",
"running": sum(1 for s in [etl.get("airflow_healthy"), etl.get("kafka_ui_ok"), etl.get("spark_ui_ok")] if s),
"total": 3,
"apps": etl_apps,
"vm": "airflow + kafka",
"ip": "10.0.21.55 / .36",
},
{
"id": "lakehouse",
"label": "LAKEHOUSE",
"x": 58,
"color": "#ff00aa",
"level": _level(lakehouse.get("running", 0), lakehouse.get("total", 1) or 1),
"running": lakehouse.get("running", 0),
"total": lakehouse.get("total", 0),
"apps": lake_apps,
"trino_ok": lakehouse.get("trino_ok"),
"vm": "atc-lake01",
"ip": lakehouse.get("host", "10.0.21.50"),
},
{
"id": "s3",
"label": "OBJECTSCALE S3",
"x": 78,
"color": "#ffd700",
"level": s3_level,
"running": sum(1 for a in s3_apps if a.get("state") == "running"),
"total": len(s3_apps),
"apps": s3_apps,
"vm": "atc-objectscale",
"ip": OBJECTSCALE_HOST,
"bucket": OBJECTSCALE_BUCKET,
},
{
"id": "hadoop",
"label": "HADOOP HDFS",
"x": 50,
"color": "#39ff14",
"level": "ok" if hdfs_ok else "warn",
"running": hadoop.get("live_datanodes", 0),
"total": (hadoop.get("live_datanodes") or 0) + (hadoop.get("dead_datanodes") or 0) or 3,
"apps": [
{"name": "NameNode", "state": "running" if hdfs_ok else "down", "image": "hdfs-nn", "ports": ["9870"], "host": "10.0.21.61"},
*[
{"name": dn.get("host", "?").split(".")[0], "state": "running", "image": "datanode", "ports": ["9866"], "host": dn.get("host", "")}
for dn in hadoop.get("datanodes", [])
],
],
"hdfs_used_gb": hadoop.get("capacity_used_gb"),
"hdfs_total_gb": hadoop.get("capacity_total_gb"),
"vm": "hadoop cluster",
"ip": "10.0.21.6170",
},
]
def _node(
nid: str,
label: str,
vm: str,
ip: str,
x: float,
y: float,
color: str,
level: str,
role: str,
apps: list[dict],
running: int,
total: int,
extra: dict | None = None,
) -> dict[str, Any]:
reg = NODE_REGISTRY.get(nid, {})
row: dict[str, Any] = {
"id": nid,
"label": label,
"vm": vm,
"ip": ip,
"x": x,
"y": y,
"color": reg.get("color", color),
"level": level,
"role": role,
"apps": apps,
"running": running,
"total": total,
"description": reg.get("description", ""),
"agent_id": NODE_AGENT.get(nid),
"links": reg.get("links", []),
"endpoints": reg.get("endpoints", []),
"commands": reg.get("commands", []),
"vmid": reg.get("vmid"),
"pve": reg.get("pve"),
}
if extra:
row.update(extra)
return row
connect_running = 1 if connect_app and connect_app.get("state") == "running" else 0
consumer_running = 1 if s3_consumer and s3_consumer.get("state") == "running" else 0
topology_nodes = [
_node("airflow", "Airflow", "atc-airflow01", "10.0.21.55", 6, 18, "#00f0ff", "ok" if etl.get("airflow_healthy") else "warn", "orchestrator",
[{"name": "scheduler", "state": "running" if etl.get("airflow_healthy") else "down", "image": "airflow", "ports": ["8080"]}], int(etl.get("airflow_healthy", False)), 1),
_node("db", "DB Vault", "atc-db02", "10.0.21.51", 22, 18, "#ffaa00", _level(databases.get("running", 0), databases.get("total", 1) or 1), "sources",
db_apps, databases.get("running", 0), databases.get("total", 0)),
_node("debezium", "Debezium", "atc-lake01", "10.0.21.50", 38, 18, "#ff66cc", "ok" if connect_running and connectors else "warn", "cdc",
[_app_row(connect_app)] if connect_app else [], len(connectors), max(len(connectors), 1),
{"connectors": connectors}),
_node("kafka", "Kafka", "atc-kafka01", "10.0.21.36", 54, 18, "#00f0ff", "ok" if etl.get("kafka_ui_ok") else "warn", "bus",
[{"name": "broker", "state": "running" if etl.get("kafka_ui_ok") else "down", "image": "kafka", "ports": ["9092"]}], int(etl.get("kafka_ui_ok", False)), 1),
_node("lakehouse", "Lakehouse", "atc-lake01", "10.0.21.50", 70, 18, "#ff00aa", _level(lakehouse.get("running", 0), lakehouse.get("total", 1) or 1), "compute",
lake_apps, lakehouse.get("running", 0), lakehouse.get("total", 0),
{"trino_ok": lakehouse.get("trino_ok"), "spark_count": len(spark_apps)}),
_node("s3", "ObjectScale S3", "atc-objectscale", OBJECTSCALE_HOST, 88, 18, "#ffd700", s3_level, "storage",
s3_apps, sum(1 for a in s3_apps if a.get("state") == "running"), len(s3_apps),
{"bucket": OBJECTSCALE_BUCKET, "port": OBJECTSCALE_PORT, "consumer_ok": bool(consumer_running)}),
_node("docker", "Docker Rack", "atc-docker01", "10.0.21.45", 10, 52, "#b366ff", _level(docker.get("running", 0), docker.get("total", 1) or 1), "infra",
docker_apps, docker.get("running", 0), docker.get("total", 0)),
_node("hadoop", "Hadoop HDFS", "atc-hadoop-m01", "10.0.21.61", 50, 52, "#39ff14", "ok" if hdfs_ok else "warn", "parallel",
zones[-1]["apps"], hadoop.get("live_datanodes", 0), zones[-1]["total"],
{"hdfs_used_gb": hadoop.get("capacity_used_gb"), "hdfs_total_gb": hadoop.get("capacity_total_gb")}),
_node("gpu", "GPU Lab", "atc-gpu-dev", "10.0.20.106", 88, 52, "#76b900", "ok" if gpu.get("ok") else "down", "inference",
[{"name": gpu.get("active_model") or "vLLM", "state": "running" if gpu.get("inference_active") else "down", "image": "vllm", "ports": ["8001"]}],
gpu.get("gpu_count", 0), gpu.get("gpu_count", 0) or 4,
{"model": gpu.get("active_model"), "util": round(sum(g.get("util_gpu", 0) for g in gpu.get("gpus", [])) / max(len(gpu.get("gpus", [])), 1), 1)}),
_node("command", "Command Center", "MCP · VM304", "10.0.21.33", 50, 78, "#00f0ff",
_level(command.get("running", 0), command.get("total", 1) or 1), "hub",
command.get("containers") and [_app_row(c) for c in command.get("containers", [])] or [
{"name": "atc-agents-api", "state": "running", "image": "atc-agents-api", "ports": ["3201"]},
{"name": "atc-agents-ui", "state": "running", "image": "atc-agents-ui", "ports": ["80"]},
{"name": "postgres", "state": "running", "image": "postgres", "ports": ["5432"]},
{"name": "redis", "state": "running", "image": "redis", "ports": ["6379"]},
{"name": "caddy", "state": "running", "image": "caddy", "ports": ["80"]},
],
command.get("running", 5), command.get("total", 5) or 5),
]
def _edge(eid: str, src: str, dst: str, label: str, kind: str, active: bool = True) -> dict[str, Any]:
return {"id": eid, "from": src, "to": dst, "label": label, "kind": kind, "active": active}
pipeline_ok = etl.get("airflow_healthy") and len(connectors) > 0 and etl.get("kafka_ui_ok")
direct_pipeline_ok = (
pipeline_ok
and lakehouse.get("trino_ok")
and objectscale_ok
)
s3_flow_ok = pipeline_ok and consumer_running and objectscale_ok
if not s3_flow_ok and direct_pipeline_ok and not consumer_running:
# Dockhand blind (auth/outage) but ETL + Trino + S3 probes healthy
s3_flow_ok = True
topology_edges = [
_edge("e-seed", "airflow", "db", "seed data", "pipeline", bool(etl.get("airflow_healthy"))),
_edge("e-cdc", "db", "debezium", "CDC", "pipeline", bool(connectors)),
_edge("e-topics", "debezium", "kafka", "topics", "pipeline", bool(connectors and etl.get("kafka_ui_ok"))),
_edge("e-stream", "kafka", "lakehouse", "stream", "pipeline", bool(etl.get("kafka_ui_ok") and lakehouse.get("trino_ok"))),
_edge("e-s3", "lakehouse", "s3", "s3-kafka-consumer", "pipeline", bool(s3_flow_ok)),
_edge("e-iceberg", "lakehouse", "s3", "Trino Iceberg", "query", bool(lakehouse.get("trino_ok") and objectscale_ok)),
_edge("e-trino-db", "lakehouse", "db", "federated SQL", "query", bool(lakehouse.get("trino_ok"))),
_edge("e-hdfs", "lakehouse", "hadoop", "parallel layer", "parallel", bool(hdfs_ok)),
_edge("e-monitor-docker", "command", "docker", "monitor", "infra", True),
_edge("e-monitor-gpu", "command", "gpu", "LLM", "infra", bool(gpu.get("ok"))),
]
topologies = build_all_topologies(
topology_nodes,
topology_edges,
snap,
pipeline_active=s3_flow_ok,
connectors=connectors,
)
return {
"ts": snap.get("ts"),
"zones": zones,
"topology": topologies["architecture"],
"topologies": topologies,
"gpu": {
"level": "ok" if gpu.get("ok") and gpu.get("inference_active") else ("warn" if gpu.get("ok") else "down"),
"model": gpu.get("active_model"),
"inference_active": gpu.get("inference_active"),
"gpu_count": gpu.get("gpu_count", 0),
"avg_util": round(
sum(g.get("util_gpu", 0) for g in gpu.get("gpus", [])) / max(len(gpu.get("gpus", [])), 1),
1,
),
"gpus": gpu.get("gpus", []),
},
"totals": {
"apps_running": sum(z["running"] for z in zones if z["id"] not in ("hadoop",)) + (hadoop.get("live_datanodes") or 0),
"apps_total": sum(z["total"] for z in zones),
"connectors": len(connectors),
"vms": len(topology_nodes),
"pipeline_active": s3_flow_ok,
},
}