Files
atc-agents/api/presentation.py
T
mo 9008fbd512 feat: Authentik login + switchable GPU prod target
Add OIDC auth for Command Center and runtime GPU endpoint selection
pointed at atc-gpu-prod (10.0.10.106), matching what is currently deployed.
2026-07-21 23:20:24 +00:00

437 lines
18 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 live presentation deck from cluster snapshot + registry."""
from __future__ import annotations
import json
from typing import Any
from node_registry import NODE_REGISTRY
from topology_views import build_all_topologies
from workload import build_workload_payload
def _status_badge(level: str) -> str:
return {"ok": "● Online", "warn": "◐ Degraded", "down": "○ Offline", "unknown": "? Unknown"}.get(level, level)
def _slide(slide_id: str, title: str, subtitle: str, bullets: list[str], **extra: Any) -> dict[str, Any]:
return {"id": slide_id, "title": title, "subtitle": subtitle, "bullets": bullets, **extra}
def _node_slide(nid: str, extra_bullets: list[str] | None = None) -> dict[str, Any]:
reg = NODE_REGISTRY.get(nid, {})
bullets: list[str] = []
if reg.get("description"):
bullets.append(reg["description"])
bullets.append(f"VM: {reg.get('vm', '?')} (VMID {reg.get('vmid', '?')}) @ {reg.get('ip', '?')}")
for link in reg.get("links", [])[:5]:
bullets.append(f"{link.get('label', 'Link')}: {link.get('url', '')}")
for ep in reg.get("endpoints", [])[:6]:
bullets.append(f"{ep.get('name', '?')}: {ep.get('host', '?')}:{ep.get('port', '?')}")
if extra_bullets:
bullets.extend(extra_bullets)
kind = "gpu" if nid == "gpu" else "command" if nid == "command" else "zone"
if nid in ("openmetadata", "elastic"):
kind = "architecture"
return _slide(
f"node-{nid}",
reg.get("label", nid),
f"{reg.get('role', 'service').upper()} · {reg.get('vm', '')}",
bullets[:14],
kind=kind,
)
def build_presentation_payload(snap: dict[str, Any]) -> dict[str, Any]:
workload = build_workload_payload(snap)
topologies = workload.get("topologies") or build_all_topologies(snap)
totals = workload.get("totals", {})
zones = workload.get("zones", [])
gpu = workload.get("gpu", {})
etl = snap.get("etl", {})
hadoop = snap.get("hadoop", {})
objectscale = snap.get("objectscale", {})
command = snap.get("command_center", {})
databases = snap.get("databases", {})
docker = snap.get("docker", {})
lakehouse = snap.get("lakehouse", {})
governance = snap.get("governance") or {}
slides: list[dict[str, Any]] = []
slides.append(_slide(
"title",
"Dell ATC Data Lab",
"Live infrastructure presentation — Command Center",
[
f"Snapshot: {snap.get('ts', 'now')}",
f"Pipeline: {'ACTIVE' if totals.get('pipeline_active') else 'INACTIVE'}",
f"Apps running: {totals.get('apps_running', 0)}/{totals.get('apps_total', 0)}",
f"CDC connectors: {totals.get('connectors', 0)} · Source DBs: 5 engines on db02",
f"LLM: {gpu.get('model') or 'offline'} ({gpu.get('gpu_count', 0)}× V100)",
"Command Center → http://10.0.21.33/ · Data Platform tab → Presentation",
],
kind="hero",
))
slides.append(_slide(
"mission",
"Mission",
"End-to-end modern data platform on Dell infrastructure",
[
"Ingest CDC from PostgreSQL, MySQL, MongoDB (+ Cassandra & Neo4j for analytics)",
"Stream via Kafka & Debezium into Spark, Trino & Iceberg on the lakehouse",
"Land curated data on ObjectScale S3 — federated SQL with Trino",
"Govern with OpenMetadata — catalog, lineage, PII classification",
"Search & observe via Elasticsearch/Kibana · BI via Superset",
"Parallel 9-node Hadoop HDFS cluster for batch workloads",
"GPU lab (4× V100) powers autonomous agents with local vLLM inference",
"This Command Center orchestrates agents, approvals & live visibility",
],
kind="narrative",
))
slides.append(_slide(
"command-center-ui",
"Command Center UI",
"Everything you operate from this dashboard",
[
"Data Platform — interactive topology + live presentation deck",
"Data Sources UI — browse, filter & edit all 5 source databases",
"Live Changes — real-time Debezium CDC event stream",
"Data Flow — OpenMetadata catalog, lineage & pipeline map",
"Data Quality — Docling document QA + RAG ingest",
"Knowledge Chat — GPU-backed RAG over lab documentation",
"Object Storage · HDFS · Elasticsearch · SSH terminal",
"Agent fleet · Approval inbox · Live GPU matrix",
],
kind="command",
))
arch = topologies.get("architecture") or workload.get("topology") or {}
arch_nodes = arch.get("nodes", [])
slides.append(_slide(
"architecture",
"Data Platform Architecture",
arch.get("subtitle", "Sources → Ingestion → Compute → Storage → Consumers"),
[f"{n.get('label', n.get('id'))}: {n.get('subtitle', n.get('role', ''))}" for n in arch_nodes[:16]],
kind="topology",
topology=arch,
))
db_lines = [
f"Host atc-db02 (10.0.21.51) — {databases.get('running', 0)}/{databases.get('total', 0)} containers up",
"postgres_sales — PostgreSQL sales_orders (CDC → Debezium)",
"mysql_hr — MySQL employee_events (CDC → Debezium)",
"mongodb_supplychain — MongoDB supplychain.events (CDC → Debezium)",
"cassandra_telemetry — Cassandra device_metrics (Trino federated)",
"neo4j_graph — Product/Supplier graph · 4.5M nodes",
]
for c in (databases.get("containers") or [])[:8]:
db_lines.append(f" · {c.get('name', '?')}: {c.get('state', '?')}")
slides.append(_slide(
"source-databases",
"Source Databases",
"DB Vault · atc-db02 · 10.0.21.51",
db_lines,
kind="topology",
))
pipeline = topologies.get("pipeline", {})
connector_lines = [
f" · {cs['name']}: {cs.get('state', '?')}"
for cs in (etl.get("connector_status") or [])[:8]
]
slides.append(_slide(
"pipeline",
"CDC Pipeline",
pipeline.get("subtitle", "Airflow → DB → Debezium → Kafka → Lakehouse → S3"),
[
f"Airflow: {'healthy' if etl.get('airflow_healthy') else 'degraded'}{etl.get('airflow_url', 'http://10.0.21.55:8080')}",
f"Kafka UI: {'UP' if etl.get('kafka_ui_ok') else 'DOWN'} — http://10.0.21.36:9000",
f"Debezium Connect: http://10.0.21.50:8083",
f"Connectors: {', '.join(etl.get('connectors') or []) or 'none'}",
*connector_lines,
f"Spark UI: {'UP' if etl.get('spark_ui_ok') else 'DOWN'} — http://10.0.21.50:8080",
f"ObjectScale S3: {'reachable' if objectscale.get('reachable') else 'down'} — bucket={objectscale.get('bucket', 'data')}",
],
kind="topology",
topology=pipeline,
))
lake_lines = [
f"atc-lake01 @ {lakehouse.get('host', '10.0.21.50')}{lakehouse.get('running', 0)}/{lakehouse.get('total', 0)} containers",
f"Trino: {'UP' if lakehouse.get('trino_ok') else 'DOWN'} — http://10.0.21.50:8089",
"Spark — batch & streaming compute",
"Kafka Connect + Debezium — CDC ingestion",
"s3-kafka-consumer — events → ObjectScale S3",
"Iceberg catalog — bronze → silver → gold tables",
]
for c in (lakehouse.get("containers") or [])[:8]:
lake_lines.append(f" · {c.get('name', '?')}: {c.get('state', '?')}")
slides.append(_slide(
"lakehouse",
"Lakehouse Hub",
"Spark · Trino · Iceberg · Kafka Connect",
lake_lines,
kind="topology",
))
docker_lines = [
f"atc-docker01 @ 10.0.21.45 — {docker.get('running', 0)}/{docker.get('total', 0)} containers",
"Homepage — http://10.0.21.45",
"Dockhand — container management http://10.0.21.45:8082",
"Apache Superset — BI dashboards http://10.0.21.45:8088",
"Forgejo / Gitea · monitoring · nginx · redis",
]
for c in (docker.get("containers") or [])[:10]:
docker_lines.append(f" · {c.get('name', '?')}: {c.get('state', '?')}")
slides.append(_slide(
"docker-rack",
"Docker Rack & Analytics",
"Platform services on atc-docker01",
docker_lines,
kind="zone",
))
cdc = governance.get("cdc") or {}
gov_lines = [
f"OpenMetadata UI: {governance.get('openmetadata_url', 'http://10.0.21.47:8585')}",
"Ingestion Airflow: http://10.0.21.47:8080",
"Catalog · lineage · data quality · PII auto-classification (Presidio NER)",
]
if "error" not in cdc:
by_src = ", ".join(f"{k}={v}" for k, v in (cdc.get("by_source") or {}).items()) or "none"
gov_lines.append(f"CDC stream: connected={cdc.get('connected')} · {cdc.get('window_total', 0)} changes/15m ({by_src})")
ps = governance.get("pii_summary") or {}
if "error" not in ps:
gov_lines.append(f"PII: {ps.get('pii_columns', 0)} columns · {ps.get('masked_columns', 0)} masked")
for ln in (governance.get("lineage") or [])[:4]:
gov_lines.append(f"Lineage: {ln}")
slides.append(_slide(
"governance",
"Governance & Metadata",
"OpenMetadata · CDC · PII · Lineage",
gov_lines,
kind="architecture",
))
slides.append(_node_slide("openmetadata"))
slides.append(_node_slide("elastic"))
for zone in zones:
apps = zone.get("apps") or []
app_lines = [
f"{a['name']}: {a['state']}" + (f" ({a.get('host', '')})" if a.get("host") else "")
for a in apps[:12]
]
slides.append(_slide(
f"zone-{zone['id']}",
zone["label"],
f"{zone.get('vm', '')} · {zone.get('ip', '')} · {_status_badge(zone.get('level', 'unknown'))}",
[
f"Containers: {zone.get('running', 0)}/{zone.get('total', 0)} running",
*app_lines,
],
kind="zone",
zone=zone,
))
PRESENTATION_NODES = [
"airflow", "db", "debezium", "kafka", "lakehouse", "s3",
"docker", "hadoop", "gpu", "command",
]
slides.append(_slide(
"infrastructure-map",
"Infrastructure Map",
"Proxmox VMs & services across VLAN 20/21",
[
f"{NODE_REGISTRY[nid]['label']}{NODE_REGISTRY[nid].get('vm')} "
f"(VMID {NODE_REGISTRY[nid].get('vmid', '?')}) @ {NODE_REGISTRY[nid].get('ip')}"
for nid in PRESENTATION_NODES if nid in NODE_REGISTRY
],
kind="registry",
))
for nid in PRESENTATION_NODES:
if nid in NODE_REGISTRY and nid not in ("docker", "db", "lakehouse"):
slides.append(_node_slide(nid))
dn_lines = [
f" · {dn['host']}: {dn.get('used_gb', 0)} GB — {dn.get('state', '')}"
for dn in (hadoop.get("datanodes") or [])[:6]
]
slides.append(_slide(
"hadoop",
"Hadoop HDFS",
"9-node parallel storage cluster",
[
f"NameNode: {'UP' if hadoop.get('reachable') else 'DOWN'}{hadoop.get('namenode', 'http://10.0.21.61:9870')}",
f"Capacity: {hadoop.get('capacity_used_gb', '?')} / {hadoop.get('capacity_total_gb', '?')} GB",
f"DataNodes: {hadoop.get('live_datanodes', 0)} live, {hadoop.get('dead_datanodes', 0)} dead",
f"Files: {hadoop.get('files_total', 0)}, Blocks: {hadoop.get('blocks_total', 0)}",
*dn_lines,
],
kind="data",
))
gpus = gpu.get("gpus") or snap.get("gpu", {}).get("gpus") or []
try:
from gpu_config import resolve_gpu_identity
gpu_id = resolve_gpu_identity(snap.get("gpu") or gpu)
except Exception:
host = (snap.get("gpu") or gpu).get("host") or "10.0.10.106"
gpu_id = {
"vm": "atc-gpu-prod",
"vmid": 306,
"ui_url": f"http://{host}:9000",
"llm_url": f"http://{host}:8001/v1",
}
gpu_lines = [
f"GPU{g['index']}: {g.get('util_gpu', 0):.0f}% util, "
f"{g.get('memory_used_mib', 0):.0f}/{g.get('memory_total_mib', 0):.0f} MiB"
for g in gpus[:4]
]
slides.append(_slide(
"gpu",
"GPU Lab & GenAI",
f"{gpu.get('model') or 'vLLM'} on {gpu_id['vm']} (VM {gpu_id['vmid']})",
[
f"Inference: {'ON' if gpu.get('inference_active') else 'OFF'}",
f"API: {gpu_id['llm_url']}",
f"GPU Lab UI: {gpu_id['ui_url']}",
"Kibana/Elastic: http://10.0.21.46:5601",
*gpu_lines,
],
kind="gpu",
))
slides.append(_slide(
"agents",
"Autonomous Agents",
"Mo & Bart supervise 5 domain operators + MCP hub",
[
"ETL Guardian — Airflow, Kafka, Debezium, connectors",
"Data Custodian — PostgreSQL, MySQL, MongoDB, Cassandra, Neo4j",
"Lakehouse Ops — Spark, Trino, Iceberg, ObjectScale S3, OpenMetadata",
"Hadoop Ranger — HDFS NameNode, DataNodes, block health",
"Infra Sentinel — Docker rack, GPU lab, Command Center",
"All agents receive LIVE cluster snapshot in every LLM prompt",
],
kind="agents",
))
cc_apps = [f"{c['name']}: {c['state']}" for c in (command.get("containers") or [])]
slides.append(_slide(
"command",
"Command Center Stack",
"VM 304 — this dashboard runs here",
[
f"Host: {command.get('host', '10.0.21.33')} (VMID {command.get('vmid', 304)})",
f"Stack: {command.get('running', 0)}/{command.get('total', 0)} containers (api, ui, caddy, redis, postgres)",
*cc_apps,
"WebSocket ops feed · Approval inbox · Agent terminals · Data Sources UI",
],
kind="command",
))
slides.append(_slide(
"demo",
"Live Demo Tips",
"Use this deck during customer presentations",
[
"Press ← → or click dots to navigate slides · F = fullscreen",
"Data Platform tab → Presentation sub-tab (this deck)",
"Data Platform → Topology for interactive pipeline map",
"Export HTML opens a standalone deck for projectors / offline",
"Ask agents in the Command Bar — they see full cluster context",
"Trigger data generation in Data Sources UI → Generate tab",
],
kind="cta",
))
return {
"ts": snap.get("ts"),
"title": "Dell ATC Data Lab",
"subtitle": "Live Infrastructure Presentation",
"totals": totals,
"pipeline_active": totals.get("pipeline_active"),
"slides": slides,
"slide_count": len(slides),
"workload": workload,
"topologies": topologies,
}
def render_presentation_html(payload: dict[str, Any]) -> str:
slides_json = json.dumps(payload.get("slides", []), default=str)
title = payload.get("title", "ATC Lab")
ts = payload.get("ts", "")
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>{title} — Presentation</title>
<style>
*{{box-sizing:border-box;margin:0;padding:0}}
body{{font-family:system-ui,-apple-system,Segoe UI,sans-serif;background:#0c1929;color:#e8f1ff;height:100vh;overflow:hidden}}
.deck{{height:100vh;display:flex;flex-direction:column}}
header{{padding:1rem 2rem;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid rgba(96,165,250,.2);background:rgba(15,27,46,.9)}}
header h1{{font-size:1.1rem;font-weight:600}}
header .meta{{font-size:.75rem;opacity:.7}}
.slide{{flex:1;display:none;padding:3rem 4rem;overflow:auto}}
.slide.active{{display:flex;flex-direction:column;justify-content:center}}
.slide h2{{font-size:2.4rem;margin-bottom:.5rem;background:linear-gradient(90deg,#60a5fa,#a78bfa);-webkit-background-clip:text;-webkit-text-fill-color:transparent}}
.slide h3{{font-size:1rem;opacity:.75;margin-bottom:2rem;font-weight:400}}
.slide ul{{list-style:none;font-size:1.15rem;line-height:1.9}}
.slide li::before{{content:"▸ ";color:#60a5fa}}
.slide.hero h2{{font-size:3.2rem}}
.slide img.slide-img{{max-height:42vh;max-width:100%;margin-top:1.5rem;border-radius:10px;border:1px solid rgba(96,165,250,.25);box-shadow:0 8px 30px rgba(0,0,0,.4);object-fit:contain}}
nav{{display:flex;gap:.5rem;padding:1rem 2rem;border-top:1px solid rgba(96,165,250,.2);align-items:center}}
nav button{{background:#1e3a5f;border:1px solid rgba(96,165,250,.3);color:#e8f1ff;padding:.5rem 1rem;border-radius:6px;cursor:pointer}}
nav button:hover{{background:#234876}}
.dots{{display:flex;gap:6px;flex:1;justify-content:center;flex-wrap:wrap}}
.dot{{width:8px;height:8px;border-radius:50%;background:rgba(96,165,250,.3);cursor:pointer;border:none}}
.dot.active{{background:#60a5fa;transform:scale(1.3)}}
.counter{{font-size:.8rem;opacity:.6;min-width:4rem;text-align:right}}
</style>
</head>
<body>
<div class="deck">
<header><h1>{title}</h1><div class="meta">Dell ATC · Live snapshot {ts}</div></header>
<div id="slides"></div>
<nav>
<button id="prev">← Prev</button>
<div class="dots" id="dots"></div>
<button id="next">Next →</button>
<span class="counter" id="counter"></span>
</nav>
</div>
<script>
const slides={slides_json};
let i=0;
const container=document.getElementById("slides");
const dots=document.getElementById("dots");
const counter=document.getElementById("counter");
slides.forEach((s,idx)=>{{
const el=document.createElement("section");
el.className="slide"+(s.kind==="hero"?" hero":"")+(idx===0?" active":"");
const bullets=(s.bullets||[]).map(b=>"<li>"+b+"</li>").join("");
const img=s.image?'<img class="slide-img" src="'+s.image+'" alt=""/>':"";
el.innerHTML="<h2>"+s.title+"</h2><h3>"+(s.subtitle||"")+"</h3><ul>"+bullets+"</ul>"+img;
container.appendChild(el);
const d=document.createElement("button");
d.className="dot"+(idx===0?" active":"");
d.onclick=()=>go(idx);
dots.appendChild(d);
}});
function go(n){{i=Math.max(0,Math.min(slides.length-1,n));document.querySelectorAll(".slide").forEach((e,j)=>e.classList.toggle("active",j===i));document.querySelectorAll(".dot").forEach((e,j)=>e.classList.toggle("active",j===i));counter.textContent=(i+1)+"/"+slides.length;}}
document.getElementById("prev").onclick=()=>go(i-1);
document.getElementById("next").onclick=()=>go(i+1);
document.onkeydown=e=>{{if(e.key==="ArrowRight"||e.key===" ")go(i+1);if(e.key==="ArrowLeft")go(i-1);if(e.key==="f"||e.key==="F")document.documentElement.requestFullscreen?.();}};
go(0);
</script>
</body>
</html>"""