Files
ome-cockpit/api/main.py
T
mo 115b6e39aa Add Reports drawer, Dell catalog compliance, and Service Tag chat context.
Exposes OME warranties, baselines, report defs, and CSV/JSON exports for customer-ready fleet briefs.
2026-07-17 15:15:57 +02:00

2280 lines
81 KiB
Python

import asyncio
import json
import os
import sqlite3
import ipaddress
import logging
import time
from collections import defaultdict
from pathlib import Path
from typing import Any
import httpx
import asyncssh
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, Response
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings
logging.basicConfig(level=logging.INFO)
log = logging.getLogger("ome-cockpit")
class Settings(BaseSettings):
ome_url: str = "https://cov-omeprod01.dell-atc.lan"
ome_user: str = "admin"
ome_password: str = ""
poll_interval: float = 15.0
openwebui_url: str = "http://atc-portal01.dell-atc.lan:3080"
cors_origins: str = "*"
power_fetch_limit: int = 40
gpu_metrics_url: str = "http://10.0.10.106:9110"
vllm_url: str = "http://10.0.10.106:8000/v1"
vllm_model: str = "llama3-70b-gptq"
vllm_max_model_len: int = 4096
vllm_max_tokens: int = 700
chat_system_chars: int = 9000
openwebui_email: str = ""
openwebui_password: str = ""
cockpit_data: str = "/data"
reports_cache_ttl: float = 600.0
class Config:
env_file = ".env"
settings = Settings()
app = FastAPI(title="OME Cockpit API", version="1.2.0")
app.add_middleware(
CORSMiddleware,
allow_origins=[o.strip() for o in settings.cors_origins.split(",")],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
STATE: dict[str, Any] = {
"updated_at": 0,
"ome": {},
"summary": {},
"subnets": [],
"devices": [],
"groups": [],
"models": [],
"alerts": [],
"events": [],
"context": {},
"gpu": {},
"pulse": 0,
"openwebui_url": settings.openwebui_url,
}
CLIENTS: set[WebSocket] = set()
SSH_SESSIONS: dict[str, dict] = {}
SSH_MAX_SESSIONS = 25
SSH_MAX_PER_CLIENT = 3
_ssh_sem: asyncio.Semaphore | None = None
_lock = asyncio.Lock()
DETAIL_CACHE: dict[int, dict] = {}
PREV_DEVICES: dict[int, dict] = {}
EVENT_FEED: list[dict] = []
REPORT_CACHE: dict[str, Any] = {
"warranties": None,
"warranties_ts": 0.0,
"compliance": None,
"compliance_ts": 0.0,
"baselines": None,
"baselines_ts": 0.0,
"catalogs": None,
"catalogs_ts": 0.0,
"report_defs": None,
"report_defs_ts": 0.0,
"jobs": None,
"jobs_ts": 0.0,
}
async def ome_session(client: httpx.AsyncClient) -> tuple[str, dict, str]:
"""Create an OME API session; returns (base, headers, session_id)."""
base = settings.ome_url.rstrip("/")
r = await client.post(
f"{base}/api/SessionService/Sessions",
json={
"UserName": settings.ome_user,
"Password": settings.ome_password,
"SessionType": "API",
},
)
r.raise_for_status()
token = r.headers.get("X-Auth-Token")
sid = str((r.json() or {}).get("Id") or "")
headers = {"X-Auth-Token": token, "Accept": "application/json"}
return base, headers, sid
async def ome_session_delete(client: httpx.AsyncClient, base: str, headers: dict, sid: str) -> None:
if not sid:
return
try:
await client.delete(f"{base}/api/SessionService/Sessions('{sid}')", headers=headers)
except Exception:
pass
def _cache_get(key: str) -> Any | None:
ts = float(REPORT_CACHE.get(f"{key}_ts") or 0)
if REPORT_CACHE.get(key) is not None and time.time() - ts < settings.reports_cache_ttl:
return REPORT_CACHE.get(key)
return None
def _cache_set(key: str, value: Any) -> Any:
REPORT_CACHE[key] = value
REPORT_CACHE[f"{key}_ts"] = time.time()
return value
def csv_escape(val: Any) -> str:
s = "" if val is None else str(val)
if any(c in s for c in (",", '"', "\n", "\r")):
return '"' + s.replace('"', '""') + '"'
return s
def rows_to_csv(rows: list[dict], columns: list[str] | None = None) -> str:
if not rows and not columns:
return ""
cols = columns or list(rows[0].keys())
lines = [",".join(cols)]
for row in rows:
lines.append(",".join(csv_escape(row.get(c)) for c in cols))
return "\n".join(lines) + "\n"
def subnet_of(ip: str | None) -> str:
if not ip or ip.count(".") != 3:
return "unknown"
try:
return str(ipaddress.ip_network(f"{ip}/24", strict=False))
except Exception:
parts = ip.split(".")
return f"{parts[0]}.{parts[1]}.{parts[2]}.0/24"
def pick_ip(device: dict) -> str | None:
for m in device.get("DeviceManagement") or []:
addr = m.get("NetworkAddress")
if addr and addr.count(".") == 3:
return addr
return None
async def fetch_power(client: httpx.AsyncClient, base: str, headers: dict, device_id: int) -> dict:
try:
r = await client.get(f"{base}/api/DeviceService/Devices({device_id})/Power", headers=headers)
if r.status_code != 200:
return {}
p = r.json()
def num(key):
v = p.get(key)
try:
return float(v) if v not in (None, "") else None
except Exception:
return None
return {
"watts": num("power"),
"avg_watts": num("avgPower"),
"peak_watts": num("peakPower"),
"min_watts": num("minimumPower"),
"energy_kwh": num("systemEnergyConsumption"),
"power_unit": p.get("powerUnit") or "watt",
}
except Exception:
return {}
async def ome_fetch() -> dict:
base = settings.ome_url.rstrip("/")
async with httpx.AsyncClient(verify=False, timeout=60.0) as client:
r = await client.post(
f"{base}/api/SessionService/Sessions",
json={
"UserName": settings.ome_user,
"Password": settings.ome_password,
"SessionType": "API",
},
)
r.raise_for_status()
token = r.headers.get("X-Auth-Token")
sid = r.json().get("Id")
headers = {"X-Auth-Token": token, "Accept": "application/json"}
try:
info = (await client.get(f"{base}/api/ApplicationService/Info", headers=headers)).json()
devices_resp = (
await client.get(f"{base}/api/DeviceService/Devices?$top=5000", headers=headers)
).json()
groups_resp = (
await client.get(f"{base}/api/GroupService/Groups?$top=200", headers=headers)
).json()
alerts_raw = []
alerts_total = None
try:
alerts_resp = (
await client.get(
f"{base}/api/AlertService/Alerts?$top=200",
headers=headers,
)
).json()
alerts_raw = alerts_resp.get("value") or []
alerts_total = alerts_resp.get("@odata.count")
except Exception as e:
log.warning("OME alerts fetch failed: %s", e)
devices_raw = devices_resp.get("value") or []
# power for connected servers first
power_targets = [
d.get("Id")
for d in devices_raw
if d.get("Type") == 1000 and d.get("ConnectionState") and d.get("Id")
][: settings.power_fetch_limit]
# also a few offline servers for comparison
offline_ids = [
d.get("Id")
for d in devices_raw
if d.get("Type") == 1000 and not d.get("ConnectionState") and d.get("Id")
][:5]
power_targets = list(dict.fromkeys(power_targets + offline_ids))
sem = asyncio.Semaphore(8)
async def limited(did):
async with sem:
return did, await fetch_power(client, base, headers, did)
power_map = {}
if power_targets:
results = await asyncio.gather(*[limited(i) for i in power_targets])
power_map = {i: p for i, p in results if p}
finally:
if sid:
try:
await client.delete(
f"{base}/api/SessionService/Sessions('{sid}')",
headers=headers,
)
except Exception:
pass
devices = []
subnet_map: dict[str, list] = defaultdict(list)
model_counts: dict[str, int] = defaultdict(int)
idrac_count = server_count = connected = powered = 0
total_watts = 0.0
watts_samples = 0
status_counts: dict[str, int] = defaultdict(int)
for d in devices_raw:
ip = pick_ip(d)
subnet = subnet_of(ip)
dtype = d.get("Type")
sub = d.get("SubDeviceType") or ""
is_server = dtype == 1000
is_idrac = sub == "iDRAC"
conn = bool(d.get("ConnectionState"))
power_state = d.get("PowerState")
status = str(d.get("Status") or "")
status_counts[status] += 1
model = d.get("Model") or "Unknown"
if is_server:
model_counts[model] += 1
if is_idrac:
idrac_count += 1
if is_server:
server_count += 1
if conn:
connected += 1
if power_state == 17:
powered += 1
pwr = power_map.get(d.get("Id")) or {}
watts = pwr.get("watts")
if watts is not None:
total_watts += watts
watts_samples += 1
node = {
"id": d.get("Id"),
"name": d.get("DeviceName") or f"device-{d.get('Id')}",
"model": model,
"service_tag": d.get("DeviceServiceTag")
or d.get("Identifier")
or d.get("ChassisServiceTag"),
"type": dtype,
"sub_type": sub,
"connected": conn,
"power_state": power_state,
"powered_on": power_state == 17,
"status": status,
"ip": ip,
"subnet": subnet,
"is_server": is_server,
"is_idrac": is_idrac,
"idrac_url": f"https://{ip}" if ip else None,
"watts": watts,
"avg_watts": pwr.get("avg_watts"),
"peak_watts": pwr.get("peak_watts"),
"energy_kwh": pwr.get("energy_kwh"),
"last_status_time": d.get("LastStatusTime"),
"last_inventory_time": d.get("LastInventoryTime"),
}
devices.append(node)
subnet_map[subnet].append(node["id"])
subnets = []
for cidr, ids in sorted(subnet_map.items(), key=lambda x: -len(x[1])):
members = [n for n in devices if n["id"] in ids]
sub_watts = sum(n["watts"] or 0 for n in members if n.get("watts") is not None)
subnets.append({
"cidr": cidr,
"count": len(members),
"connected": sum(1 for n in members if n["connected"]),
"watts": round(sub_watts, 1) if sub_watts else None,
"device_ids": ids,
})
groups = [
{"id": g.get("Id"), "name": g.get("Name")}
for g in (groups_resp.get("value") or [])
if g.get("Name")
][:60]
models = [
{"name": m, "count": c}
for m, c in sorted(model_counts.items(), key=lambda x: -x[1])
]
# --- realtime context: fleet deltas + alerts ---
global PREV_DEVICES, EVENT_FEED
now = time.time()
new_events: list[dict] = []
first_poll = not PREV_DEVICES
current_ids: set[int] = set()
def _role(n: dict) -> str:
if n.get("is_idrac"):
return "iDRAC"
if n.get("is_server"):
return "server"
return "device"
for n in devices:
did = n.get("id")
if did is None:
continue
current_ids.add(did)
prev = PREV_DEVICES.get(did)
if not prev:
# First poll only seeds baseline; later polls raise new-node alerts
if not first_poll and (n.get("is_server") or n.get("is_idrac")):
role = _role(n)
new_events.append({
"ts": now,
"kind": "device_new",
"severity": "critical",
"device_id": did,
"title": f"New {role} discovered",
"text": (
f"{n.get('name')} · {n.get('ip') or 'no IP'} · "
f"{n.get('model') or 'unknown model'} · {n.get('subnet') or 'unknown subnet'}"
),
"notify": True,
"role": role,
"ip": n.get("ip"),
"model": n.get("model"),
"subnet": n.get("subnet"),
"name": n.get("name"),
})
continue
if bool(prev.get("connected")) != bool(n.get("connected")):
new_events.append({
"ts": now,
"kind": "connection",
"severity": "info" if n.get("connected") else "warning",
"device_id": did,
"title": n.get("name"),
"text": "Connected" if n.get("connected") else "Went offline",
})
if bool(prev.get("powered_on")) != bool(n.get("powered_on")):
new_events.append({
"ts": now,
"kind": "power_state",
"severity": "info" if n.get("powered_on") else "warning",
"device_id": did,
"title": n.get("name"),
"text": "Powered on" if n.get("powered_on") else "Powered off",
})
pw, pp = n.get("watts"), prev.get("watts")
if pw is not None and pp is not None and abs(pw - pp) >= 40:
new_events.append({
"ts": now,
"kind": "power_watt",
"severity": "info",
"device_id": did,
"title": n.get("name"),
"text": f"Power {round(pp)}W → {round(pw)}W",
})
if str(prev.get("status")) != str(n.get("status")):
new_events.append({
"ts": now,
"kind": "status",
"severity": "warning",
"device_id": did,
"title": n.get("name"),
"text": f"Status {prev.get('status')}{n.get('status')}",
})
if not first_poll:
for did, prev in PREV_DEVICES.items():
if did in current_ids:
continue
if not (prev.get("is_server") or prev.get("is_idrac")):
continue
role = _role(prev)
new_events.append({
"ts": now,
"kind": "device_removed",
"severity": "warning",
"device_id": did,
"title": f"{role} left inventory",
"text": (
f"{prev.get('name')} · {prev.get('ip') or 'no IP'} · "
f"{prev.get('model') or 'unknown model'}"
),
"notify": True,
"role": role,
"name": prev.get("name"),
"ip": prev.get("ip"),
"model": prev.get("model"),
})
PREV_DEVICES = {n["id"]: n for n in devices if n.get("id") is not None}
if new_events:
EVENT_FEED = (new_events + EVENT_FEED)[:120]
sev_map = {"Critical": 0, "Warning": 0, "Normal": 0, "Info": 0, "Unknown": 0}
alerts = []
for a in alerts_raw:
sev = a.get("SeverityName") or "Unknown"
if sev not in sev_map:
sev_map[sev] = 0
sev_map[sev] = sev_map.get(sev, 0) + 1
alerts.append({
"id": a.get("Id"),
"severity": sev,
"device_id": a.get("AlertDeviceId") or a.get("AlertEntityId"),
"device": a.get("AlertDeviceName") or a.get("AlertEntityName"),
"ip": a.get("AlertDeviceIpAddress"),
"category": a.get("CategoryName"),
"subcategory": a.get("SubCategoryName"),
"message": a.get("Message"),
"message_id": a.get("AlertMessageId"),
"status": a.get("StatusName"),
"time": a.get("TimeStamp"),
"action": a.get("RecommendedAction"),
})
hottest = sorted(
[n for n in devices if n.get("watts") is not None],
key=lambda x: x.get("watts") or 0,
reverse=True,
)[:5]
recent_status = sorted(
[n for n in devices if n.get("last_status_time")],
key=lambda x: str(x.get("last_status_time")),
reverse=True,
)[:5]
context = {
"refreshed_at": now,
"poll_seconds": settings.poll_interval,
"alerts_total": alerts_total,
"alert_severity": sev_map,
"events_new": len(new_events),
"notifications": [e for e in new_events if e.get("notify")],
"hottest": [
{"id": n["id"], "name": n["name"], "watts": n["watts"], "subnet": n["subnet"]}
for n in hottest
],
"recent_status": [
{
"id": n["id"],
"name": n["name"],
"time": n.get("last_status_time"),
"connected": n.get("connected"),
"status": n.get("status"),
}
for n in recent_status
],
"focus_hint": (
f"{connected} connected · {round(total_watts)} W sampled · "
f"{sev_map.get('Critical', 0)} critical / {sev_map.get('Warning', 0)} warning alerts in feed"
),
}
return {
"ome": {
"name": info.get("Name") or "OpenManage Enterprise",
"version": info.get("Version"),
"build": info.get("BuildNumber"),
"url": base,
"fqdn": "cov-omeprod01.dell-atc.lan",
"console_url": f"{base}/management-console/ome/",
},
"summary": {
"total": len(devices),
"idracs": idrac_count,
"servers": server_count,
"connected": connected,
"powered_on": powered,
"offline": max(0, server_count - connected),
"status_counts": dict(status_counts),
"total_watts": round(total_watts, 1),
"avg_node_watts": round(total_watts / watts_samples, 1) if watts_samples else None,
"power_samples": watts_samples,
"alerts_critical": sev_map.get("Critical", 0),
"alerts_warning": sev_map.get("Warning", 0),
"alerts_total": alerts_total,
},
"subnets": subnets,
"devices": devices,
"groups": groups,
"models": models,
"alerts": alerts,
"events": EVENT_FEED[:40],
"context": context,
"openwebui_url": settings.openwebui_url,
"updated_at": now,
}
async def broadcast(payload: dict):
dead = []
for ws in list(CLIENTS):
try:
await ws.send_json(payload)
except Exception:
dead.append(ws)
for ws in dead:
CLIENTS.discard(ws)
ADMINS = [
{"id": "jody", "name": "Jody van Dongen", "role": "ATC Datacenter Admin", "email": "jody.van.dongen@dell.com"},
{"id": "laurens", "name": "Laurens Rammers", "role": "ATC Datacenter Admin", "email": "laurens.rammers@dell.com"},
]
DATA_DIR = Path(settings.cockpit_data)
DB_PATH = DATA_DIR / "ops.db"
def _db():
DATA_DIR.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys=ON")
return conn
def init_db():
with _db() as conn:
conn.executescript(
"""
CREATE TABLE IF NOT EXISTS tickets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
body TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'open',
priority TEXT NOT NULL DEFAULT 'normal',
created_by TEXT NOT NULL,
assignee TEXT,
accepted_by TEXT,
created_at REAL NOT NULL,
updated_at REAL NOT NULL
);
CREATE TABLE IF NOT EXISTS ticket_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ticket_id INTEGER NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
author TEXT NOT NULL,
body TEXT NOT NULL,
created_at REAL NOT NULL
);
"""
)
cols = {r[1] for r in conn.execute("PRAGMA table_info(tickets)").fetchall()}
if "accepted_by" not in cols:
conn.execute("ALTER TABLE tickets ADD COLUMN accepted_by TEXT")
async def fetch_gpu() -> dict:
url = settings.gpu_metrics_url.rstrip("/") + "/api/gpu"
try:
async with httpx.AsyncClient(timeout=4.0) as client:
r = await client.get(url)
if r.status_code == 200:
return r.json()
except Exception as e:
log.warning("GPU metrics fetch failed: %s", e)
return {
"host": "atc-gpu-prod",
"gpus": [],
"error": "unreachable",
"updated_at": time.time(),
"model": settings.vllm_model,
}
async def poll_loop():
while True:
try:
data = await ome_fetch()
gpu = await fetch_gpu()
data["gpu"] = gpu
async with _lock:
STATE.update(data)
STATE["pulse"] = int(STATE.get("pulse") or 0) + 1
await broadcast({"type": "snapshot", "data": {**STATE}})
log.info(
"OME refresh: %s devices, %s connected, %s W sampled, GPU util avg %s",
STATE["summary"].get("total"),
STATE["summary"].get("connected"),
STATE["summary"].get("total_watts"),
(gpu.get("summary") or {}).get("avg_util"),
)
except Exception as e:
log.exception("OME poll failed: %s", e)
await broadcast({"type": "error", "message": str(e)})
await asyncio.sleep(settings.poll_interval)
async def gpu_loop():
"""Faster GPU pulse so matrix feels live during inference."""
while True:
try:
gpu = await fetch_gpu()
async with _lock:
STATE["gpu"] = gpu
await broadcast({"type": "gpu", "data": gpu})
except Exception as e:
log.warning("gpu loop: %s", e)
await asyncio.sleep(2.0)
@app.on_event("startup")
async def startup():
init_db()
asyncio.create_task(poll_loop())
asyncio.create_task(gpu_loop())
asyncio.create_task(warm_reports_cache())
async def warm_reports_cache():
"""Background warm of warranty/compliance for chat + inspector."""
await asyncio.sleep(8)
try:
await fetch_warranties()
await fetch_compliance()
log.info(
"Reports cache warmed: warranties=%s compliance_outdated=%s",
(REPORT_CACHE.get("warranties") or {}).get("count"),
((REPORT_CACHE.get("compliance") or {}).get("summary") or {}).get("outdated_devices"),
)
except Exception as e:
log.warning("Reports cache warm failed: %s", e)
@app.get("/api/health")
async def health():
return {
"status": "ok",
"updated_at": STATE.get("updated_at"),
"pulse": STATE.get("pulse"),
}
@app.get("/api/fleet")
async def fleet():
return {**STATE}
@app.get("/api/devices/{device_id}")
async def device_detail(device_id: int):
# lightweight detail: return fleet node + optional inventory slices
node = next((d for d in STATE.get("devices") or [] if d.get("id") == device_id), None)
if not node:
raise HTTPException(404, "Device not found in current fleet snapshot")
if device_id in DETAIL_CACHE and time.time() - DETAIL_CACHE[device_id].get("_ts", 0) < 300:
return DETAIL_CACHE[device_id]
base = settings.ome_url.rstrip("/")
detail = {"device": node, "inventory": {}, "power": {}}
async with httpx.AsyncClient(verify=False, timeout=60.0) as client:
r = await client.post(
f"{base}/api/SessionService/Sessions",
json={
"UserName": settings.ome_user,
"Password": settings.ome_password,
"SessionType": "API",
},
)
r.raise_for_status()
token = r.headers.get("X-Auth-Token")
sid = r.json().get("Id")
headers = {"X-Auth-Token": token, "Accept": "application/json"}
try:
detail["power"] = await fetch_power(client, base, headers, device_id)
# Discover all inventory types OME has for this device (app/firmware landscape)
inv_types: list[str] = []
try:
tr = await client.get(
f"{base}/api/DeviceService/Devices({device_id})/InventoryTypes",
headers=headers,
)
if tr.status_code == 200:
inv_types = list((tr.json() or {}).get("InventoryTypes") or [])
except Exception:
inv_types = []
# Ensure core + software landscape types are always attempted
for required in (
"serverProcessors",
"serverMemoryDevices",
"serverArrayDisks",
"serverPowerSupplies",
"serverNetworkInterfaces",
"deviceLocation",
"deviceManagement",
"subsystemRollupStatus",
"serverOperatingSystems",
"deviceSoftware",
"deviceLicense",
"deviceFru",
"deviceCapabilities",
"serverRaidControllers",
"serverDeviceCards",
"deviceBaseboards",
"serverDellVideos",
"serverFcCards",
"serverVirtualFlashes",
"serverStorageEnclosures",
"serverSupportedPowerStates",
"serverBiosSystemProfileSettings",
"deviceInventory",
):
if required not in inv_types:
inv_types.append(required)
detail["inventory_types"] = inv_types
for inv_type in inv_types:
try:
ir = await client.get(
f"{base}/api/DeviceService/Devices({device_id})/InventoryDetails('{inv_type}')",
headers=headers,
)
if ir.status_code == 200:
info = ir.json().get("InventoryInfo") or []
if info:
detail["inventory"][inv_type] = info
except Exception:
pass
# Normalized application / firmware landscape view
software = detail["inventory"].get("deviceSoftware") or []
os_info = detail["inventory"].get("serverOperatingSystems") or []
mgmt = detail["inventory"].get("deviceManagement") or []
licenses = detail["inventory"].get("deviceLicense") or []
def _stype(s: dict) -> str:
return str(s.get("SoftwareType") or "").strip().upper()
fw_types = {"BIOS", "FRMW", "FIRMWARE", "IDRAC", "USC", "BMC", "CPLD", "DCSM"}
drv_types = {"DRVR", "DRIVER", "DRV"}
firmware = [s for s in software if _stype(s) in fw_types or "FW" in _stype(s)]
drivers = [s for s in software if _stype(s) in drv_types or "DRIVER" in _stype(s)]
fw_ids = {id(s) for s in firmware}
drv_ids = {id(s) for s in drivers}
applications = [s for s in software if id(s) not in fw_ids and id(s) not in drv_ids]
detail["landscape"] = {
"os": os_info,
"software": software,
"management": mgmt,
"licenses": licenses,
"firmware": firmware,
"drivers": drivers,
"applications": applications,
}
finally:
if sid:
try:
await client.delete(
f"{base}/api/SessionService/Sessions('{sid}')",
headers=headers,
)
except Exception:
pass
detail["_ts"] = time.time()
DETAIL_CACHE[device_id] = detail
return detail
@app.websocket("/ws/fleet")
async def ws_fleet(ws: WebSocket):
await ws.accept()
CLIENTS.add(ws)
try:
await ws.send_json({"type": "snapshot", "data": {**STATE}})
while True:
await ws.receive_text()
except WebSocketDisconnect:
pass
finally:
CLIENTS.discard(ws)
class ChatIn(BaseModel):
message: str
history: list[dict] = Field(default_factory=list)
focus_device_id: int | None = None
model: str | None = None
class TicketIn(BaseModel):
title: str
body: str
created_by: str
assignee: str | None = None
priority: str = "normal"
class TicketMsgIn(BaseModel):
author: str
body: str
class TicketPatch(BaseModel):
status: str | None = None
assignee: str | None = None
priority: str | None = None
accepted_by: str | None = None
def build_fleet_context(focus_device_id: int | None = None, max_chars: int | None = None) -> str:
summary = STATE.get("summary") or {}
gpu = STATE.get("gpu") or {}
gsum = gpu.get("summary") or {}
devices = STATE.get("devices") or []
alerts = STATE.get("alerts") or []
events = STATE.get("events") or []
subnets = STATE.get("subnets") or []
ctx = STATE.get("context") or {}
limit = max_chars if max_chars is not None else settings.chat_system_chars
connected = [d for d in devices if d.get("connected")]
critical = [a for a in alerts if a.get("severity") == "Critical"][:8]
warnings = [a for a in alerts if a.get("severity") == "Warning"][:5]
hottest = (ctx.get("hottest") or [])[:5]
compliance = _cache_get("compliance") or REPORT_CACHE.get("compliance") or {}
comp_sum = (compliance or {}).get("summary") or {}
# SERVICE TAG INDEX first — must survive truncation
st_lines = [
"SERVICE TAG INDEX (use these Service Tags in every node answer):",
]
for d in sorted(devices, key=lambda x: (x.get("name") or "").lower()):
st = (d.get("service_tag") or "").strip() or "NONE"
st_lines.append(
"- ST={st} | {name} | ip={ip} | model={model} | connected={conn}".format(
st=st,
name=(d.get("name") or "")[:40],
ip=d.get("ip") or "—",
model=(d.get("model") or "")[:28],
conn="yes" if d.get("connected") else "no",
)
)
st_block = "\n".join(st_lines)
lines = [
"You are OpenManage Cockpit Copilot for Dell ATC. Be concise and operational.",
"Admins: Jody van Dongen, Laurens Rammers, Mohamed El Kadi.",
"Use ONLY this live snapshot. Unknown => say unknown.",
"ALWAYS include Service Tag (ST=...), model, and IP when discussing any system.",
"If a Service Tag is missing in the index, say so explicitly.",
"",
"FLEET: total={t} connected={c} offline={o} watts={w} alerts_total={a}".format(
t=summary.get("total"),
c=summary.get("connected"),
o=summary.get("offline"),
w=summary.get("total_watts"),
a=summary.get("alerts_total"),
),
]
if comp_sum:
lines.append(
"FIRMWARE COMPLIANCE (Dell catalog baseline): outdated_devices={od} critical_components={cc} baseline={bn}".format(
od=comp_sum.get("outdated_devices"),
cc=comp_sum.get("critical_components"),
bn=(comp_sum.get("baseline_name") or "")[:40],
)
)
lines.append("CONNECTED:")
for d in connected[:18]:
lines.append(
"- ST={st} {name} ip={ip} model={model} W={watts} status={status}".format(
st=(d.get("service_tag") or "NONE"),
name=(d.get("name") or "")[:36],
ip=d.get("ip"),
model=(d.get("model") or "")[:28],
watts=d.get("watts"),
status=d.get("status"),
)
)
lines.append("SUBNETS:")
for s in subnets[:8]:
lines.append(
"- {cidr}: {conn}/{cnt} W={watts}".format(
cidr=s.get("cidr"),
conn=s.get("connected"),
cnt=s.get("count"),
watts=s.get("watts"),
)
)
lines.append("CRITICAL:")
if not critical:
lines.append("(none)")
for a in critical:
lines.append(
"- {dev} ({ip}): {msg}".format(
dev=(a.get("device") or "")[:32],
ip=a.get("ip"),
msg=(a.get("message") or "")[:110],
)
)
lines.append("WARNINGS:")
for a in warnings:
lines.append(
"- {dev} ({ip}): {msg}".format(
dev=(a.get("device") or "")[:32],
ip=a.get("ip"),
msg=(a.get("message") or "")[:90],
)
)
lines.append("DELTAS:")
if not events:
lines.append("(none)")
for e in events[:8]:
lines.append("- {title}: {text}".format(title=e.get("title"), text=(e.get("text") or "")[:100]))
lines.append("HOT POWER:")
for h in hottest:
lines.append("- {name}: {watts}W".format(name=(h.get("name") or "")[:28], watts=h.get("watts")))
lines.append(
"GPU atc-gpu-prod: avg_util={u}% power={p}W mem={mu}/{mt}MB".format(
u=gsum.get("avg_util"),
p=gsum.get("total_power_w"),
mu=gsum.get("total_mem_used_mb"),
mt=gsum.get("total_mem_mb"),
)
)
for g in (gpu.get("gpus") or [])[:8]:
lines.append(
"- GPU{i}: {util}% {temp}C {pw}W".format(
i=g.get("index"),
util=g.get("util_gpu"),
temp=g.get("temp_c"),
pw=g.get("power_w"),
)
)
if focus_device_id is not None:
node = next((d for d in devices if d.get("id") == focus_device_id), None)
lines.append("FOCUS:")
if node:
lines.append(
"ST={st} {name} id={id} ip={ip} model={model} W={watts} connected={conn} status={stt}".format(
st=(node.get("service_tag") or "NONE"),
name=node.get("name"),
id=node.get("id"),
ip=node.get("ip"),
model=node.get("model"),
watts=node.get("watts"),
conn=node.get("connected"),
stt=node.get("status"),
)
)
related = [a for a in alerts if a.get("device_id") == focus_device_id][:5]
for a in related:
lines.append(
"alert {sev}: {msg}".format(sev=a.get("severity"), msg=(a.get("message") or "")[:100])
)
else:
lines.append("device_id=%s missing" % focus_device_id)
lines.append(
"Reply with concrete Service Tags, names, IPs, models, and next actions."
)
body = "\n".join(lines)
budget = max(800, limit - len(st_block) - 40)
if len(body) > budget:
body = body[: budget - 20] + "\n…[truncated]"
out = st_block + "\n\n" + body
if len(out) > limit:
out = out[: limit - 20] + "\n…[truncated]"
return out
@app.get("/api/gpu")
async def api_gpu():
if not STATE.get("gpu"):
STATE["gpu"] = await fetch_gpu()
return STATE.get("gpu") or {}
@app.get("/api/admins")
async def api_admins():
return {"admins": ADMINS}
_OWUI_TOKEN: dict[str, Any] = {"token": None, "expires": 0.0}
async def _owui_token() -> str | None:
email = (settings.openwebui_email or "").strip()
password = settings.openwebui_password or ""
if not email or not password:
return None
now = time.time()
if _OWUI_TOKEN.get("token") and float(_OWUI_TOKEN.get("expires") or 0) > now:
return str(_OWUI_TOKEN["token"])
url = settings.openwebui_url.rstrip("/") + "/api/v1/auths/signin"
try:
async with httpx.AsyncClient(timeout=20.0) as client:
r = await client.post(url, json={"email": email, "password": password})
if r.status_code >= 400:
log.warning("Open WebUI signin failed: %s %s", r.status_code, r.text[:200])
return None
token = (r.json() or {}).get("token")
if token:
_OWUI_TOKEN["token"] = token
_OWUI_TOKEN["expires"] = now + 3500
return token
except Exception as e:
log.warning("Open WebUI signin error: %s", e)
return None
async def list_chat_models() -> list[dict]:
models: list[dict] = []
seen: set[str] = set()
token = await _owui_token()
if token:
try:
async with httpx.AsyncClient(timeout=15.0) as client:
r = await client.get(
settings.openwebui_url.rstrip("/") + "/api/models",
headers={"Authorization": f"Bearer {token}"},
)
if r.status_code == 200:
for m in (r.json() or {}).get("data") or []:
mid = m.get("id")
if not mid or mid in seen:
continue
seen.add(mid)
models.append(
{
"id": mid,
"name": m.get("name") or mid,
"source": "openwebui",
"owned_by": m.get("owned_by"),
}
)
except Exception as e:
log.warning("Open WebUI models failed: %s", e)
try:
async with httpx.AsyncClient(timeout=10.0) as client:
r = await client.get(settings.vllm_url.rstrip("/") + "/models")
if r.status_code == 200:
for m in (r.json() or {}).get("data") or []:
mid = m.get("id")
if not mid or mid in seen:
continue
seen.add(mid)
models.append(
{
"id": mid,
"name": mid,
"source": "vllm",
"owned_by": m.get("owned_by") or "vllm",
}
)
except Exception as e:
log.warning("vLLM models failed: %s", e)
if not models:
models.append(
{
"id": settings.vllm_model,
"name": settings.vllm_model,
"source": "vllm",
"owned_by": "vllm",
}
)
return models
@app.get("/api/models")
async def api_models():
models = await list_chat_models()
return {"models": models, "default": settings.vllm_model}
@app.post("/api/chat")
async def api_chat(payload: ChatIn):
gpu = STATE.get("gpu") or {}
gsum = gpu.get("summary") or {}
model = (payload.model or settings.vllm_model or "").strip() or settings.vllm_model
system = build_fleet_context(payload.focus_device_id, max_chars=settings.chat_system_chars)
messages = [{"role": "system", "content": system}]
for h in (payload.history or [])[-4:]:
role = h.get("role")
content = h.get("content")
if role in ("user", "assistant") and content:
messages.append({"role": role, "content": str(content)[:1200]})
messages.append({"role": "user", "content": payload.message[:2500]})
owui_models = {"ome-copilot", "arena-model"}
use_owui = model in owui_models
try:
if use_owui:
token = await _owui_token()
if not token:
raise HTTPException(
502,
"Open WebUI auth not configured (set OPENWEBUI_EMAIL / OPENWEBUI_PASSWORD)",
)
url = settings.openwebui_url.rstrip("/") + "/api/chat/completions"
async with httpx.AsyncClient(timeout=180.0) as client:
r = await client.post(
url,
headers={"Authorization": f"Bearer {token}"},
json={
"model": model,
"messages": messages,
"temperature": 0.2,
"max_tokens": settings.vllm_max_tokens,
"stream": False,
},
)
if r.status_code >= 400:
raise HTTPException(502, f"Open WebUI error {r.status_code}: {r.text[:500]}")
data = r.json()
backend = "openwebui"
else:
url = settings.vllm_url.rstrip("/") + "/chat/completions"
async with httpx.AsyncClient(timeout=180.0) as client:
r = await client.post(
url,
json={
"model": model,
"messages": messages,
"temperature": 0.2,
"max_tokens": settings.vllm_max_tokens,
},
)
if r.status_code >= 400:
detail = r.text
if r.status_code == 400 and "context length" in detail.lower():
tight = build_fleet_context(payload.focus_device_id, max_chars=3200)
messages = [{"role": "system", "content": tight}, messages[-1]]
r = await client.post(
url,
json={
"model": model,
"messages": messages,
"temperature": 0.2,
"max_tokens": 500,
},
)
detail = r.text
if r.status_code >= 400:
raise HTTPException(502, f"vLLM error {r.status_code}: {detail[:600]}")
data = r.json()
backend = "vllm"
text_out = (
((data.get("choices") or [{}])[0].get("message") or {}).get("content")
or ""
)
return {
"reply": text_out,
"model": model,
"backend": backend,
"gpu": gsum,
"context_bytes": len(system),
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(502, f"Chat backend error: {e}") from e
def _admin_name(aid: str) -> str:
for a in ADMINS:
if a["id"] == aid:
return a["name"]
return aid
@app.get("/api/tickets")
async def list_tickets():
with _db() as conn:
rows = conn.execute(
"SELECT * FROM tickets ORDER BY updated_at DESC LIMIT 200"
).fetchall()
return {"tickets": [dict(r) for r in rows], "admins": ADMINS}
@app.post("/api/tickets")
async def create_ticket(payload: TicketIn):
now = time.time()
assignee = payload.assignee
if not assignee:
assignee = "laurens" if payload.created_by == "jody" else "jody"
with _db() as conn:
cur = conn.execute(
"""
INSERT INTO tickets(title, body, status, priority, created_by, assignee, created_at, updated_at)
VALUES (?, ?, 'open', ?, ?, ?, ?, ?)
""",
(
payload.title.strip()[:200],
payload.body.strip()[:5000],
payload.priority,
payload.created_by,
assignee,
now,
now,
),
)
tid = cur.lastrowid
conn.execute(
"INSERT INTO ticket_messages(ticket_id, author, body, created_at) VALUES (?, ?, ?, ?)",
(tid, payload.created_by, payload.body.strip()[:5000], now),
)
row = conn.execute("SELECT * FROM tickets WHERE id=?", (tid,)).fetchone()
return dict(row)
@app.get("/api/tickets/{ticket_id}")
async def get_ticket(ticket_id: int):
with _db() as conn:
row = conn.execute("SELECT * FROM tickets WHERE id=?", (ticket_id,)).fetchone()
if not row:
raise HTTPException(404, "Ticket not found")
msgs = conn.execute(
"SELECT * FROM ticket_messages WHERE ticket_id=? ORDER BY created_at ASC",
(ticket_id,),
).fetchall()
return {"ticket": dict(row), "messages": [dict(m) for m in msgs], "admins": ADMINS}
@app.post("/api/tickets/{ticket_id}/messages")
async def add_ticket_message(ticket_id: int, payload: TicketMsgIn):
now = time.time()
with _db() as conn:
row = conn.execute("SELECT id FROM tickets WHERE id=?", (ticket_id,)).fetchone()
if not row:
raise HTTPException(404, "Ticket not found")
conn.execute(
"INSERT INTO ticket_messages(ticket_id, author, body, created_at) VALUES (?, ?, ?, ?)",
(ticket_id, payload.author, payload.body.strip()[:5000], now),
)
conn.execute("UPDATE tickets SET updated_at=? WHERE id=?", (now, ticket_id))
msgs = conn.execute(
"SELECT * FROM ticket_messages WHERE ticket_id=? ORDER BY created_at ASC",
(ticket_id,),
).fetchall()
return {"messages": [dict(m) for m in msgs]}
@app.patch("/api/tickets/{ticket_id}")
async def patch_ticket(ticket_id: int, payload: TicketPatch):
now = time.time()
with _db() as conn:
row = conn.execute("SELECT * FROM tickets WHERE id=?", (ticket_id,)).fetchone()
if not row:
raise HTTPException(404, "Ticket not found")
status = payload.status or row["status"]
assignee = payload.assignee if payload.assignee is not None else row["assignee"]
priority = payload.priority or row["priority"]
# accepted_by: keep existing unless explicitly set; accepting sets status accepted
accepted_by = row["accepted_by"] if "accepted_by" in row.keys() else None
if payload.accepted_by is not None:
accepted_by = payload.accepted_by
if status == "open":
status = "accepted"
conn.execute(
"UPDATE tickets SET status=?, assignee=?, priority=?, accepted_by=?, updated_at=? WHERE id=?",
(status, assignee, priority, accepted_by, now, ticket_id),
)
if payload.accepted_by:
conn.execute(
"INSERT INTO ticket_messages(ticket_id, author, body, created_at) VALUES (?, ?, ?, ?)",
(
ticket_id,
payload.accepted_by,
f"Ticket accepted by {_admin_name(payload.accepted_by)}",
now,
),
)
row = conn.execute("SELECT * FROM tickets WHERE id=?", (ticket_id,)).fetchone()
return dict(row)
@app.delete("/api/tickets/{ticket_id}")
async def delete_ticket(ticket_id: int):
with _db() as conn:
row = conn.execute("SELECT id FROM tickets WHERE id=?", (ticket_id,)).fetchone()
if not row:
raise HTTPException(404, "Ticket not found")
conn.execute("DELETE FROM ticket_messages WHERE ticket_id=?", (ticket_id,))
conn.execute("DELETE FROM tickets WHERE id=?", (ticket_id,))
return {"ok": True, "deleted": ticket_id}
def _get_ssh_sem() -> asyncio.Semaphore:
global _ssh_sem
if _ssh_sem is None:
_ssh_sem = asyncio.Semaphore(SSH_MAX_SESSIONS)
return _ssh_sem
def _fleet_ssh_targets() -> set[str]:
"""Allow SSH only to IPs currently known in the OME fleet snapshot."""
allowed: set[str] = set()
for d in STATE.get("devices") or []:
ip = d.get("ip")
if ip and ip.count(".") == 3:
allowed.add(ip)
return allowed
@app.websocket("/ws/ssh")
async def ws_ssh(ws: WebSocket):
"""Browser terminal <-> SSH bridge. Isolated per connection; concurrency-limited."""
await ws.accept()
conn = None
process = None
reader_task = None
session_id = f"{id(ws)}-{time.time()}"
client_host = ""
acquired = False
async def send_json(payload: dict):
try:
await ws.send_text(json.dumps(payload))
except Exception:
pass
def _client_key() -> str:
try:
return ws.client.host if ws.client else "unknown"
except Exception:
return "unknown"
try:
# Limit concurrent sessions globally and per browser IP
ck = _client_key()
active_for_client = sum(1 for s in SSH_SESSIONS.values() if s.get("client") == ck)
if active_for_client >= SSH_MAX_PER_CLIENT:
await send_json({
"type": "error",
"message": f"Too many SSH sessions from this client (max {SSH_MAX_PER_CLIENT})",
})
await ws.close()
return
try:
await asyncio.wait_for(_get_ssh_sem().acquire(), timeout=8.0)
acquired = True
except asyncio.TimeoutError:
await send_json({
"type": "error",
"message": "SSH gateway busy — try again in a moment",
})
await ws.close()
return
SSH_SESSIONS[session_id] = {
"client": ck,
"started": time.time(),
"host": None,
"user": None,
}
raw = await asyncio.wait_for(ws.receive_text(), timeout=60.0)
try:
msg = json.loads(raw)
except Exception:
await send_json({"type": "error", "message": "Invalid auth payload"})
await ws.close()
return
if msg.get("type") != "auth":
await send_json({"type": "error", "message": "Expected auth message"})
await ws.close()
return
host = str(msg.get("host") or "").strip()
username = str(msg.get("username") or "").strip()
password = str(msg.get("password") or "")
try:
port = int(msg.get("port") or 22)
except Exception:
port = 22
if not host or not username:
await send_json({"type": "error", "message": "Host and username are required"})
await ws.close()
return
if port < 1 or port > 65535:
await send_json({"type": "error", "message": "Invalid port"})
await ws.close()
return
async with _lock:
allowed = set(_fleet_ssh_targets())
if host not in allowed:
await send_json({
"type": "error",
"message": f"Host {host} is not in the current OME fleet — SSH blocked",
})
await ws.close()
return
client_host = host
SSH_SESSIONS[session_id]["host"] = host
SSH_SESSIONS[session_id]["user"] = username
await send_json({"type": "status", "message": f"Connecting to {username}@{host}:{port}…"})
try:
conn = await asyncio.wait_for(
asyncssh.connect(
host,
port=port,
username=username,
password=password,
known_hosts=None,
client_keys=None,
preferred_auth=["password", "keyboard-interactive"],
keepalive_interval=30,
keepalive_count_max=3,
),
timeout=25.0,
)
except Exception as e:
await send_json({"type": "error", "message": f"SSH connect failed: {e}"})
await ws.close()
return
term = str(msg.get("term") or "xterm-256color")
cols = int(msg.get("cols") or 120)
rows = int(msg.get("rows") or 36)
process = await conn.create_process(
term_type=term,
term_size=(max(cols, 40), max(rows, 10)),
encoding="utf-8",
errors="replace",
)
await send_json({"type": "ready", "message": f"Connected as {username}@{host}"})
log.info("SSH session start id=%s client=%s target=%s@%s active=%s",
session_id, ck, username, host, len(SSH_SESSIONS))
async def pump_ssh_to_ws():
try:
while True:
data = await process.stdout.read(8192)
if not data:
try:
err = await asyncio.wait_for(process.stderr.read(1024), timeout=0.2)
except Exception:
err = ""
if err:
await ws.send_text(err if isinstance(err, str) else err.decode("utf-8", "replace"))
break
await ws.send_text(data if isinstance(data, str) else data.decode("utf-8", "replace"))
except Exception:
pass
try:
await send_json({"type": "status", "message": "SSH session ended"})
except Exception:
pass
reader_task = asyncio.create_task(pump_ssh_to_ws())
# Idle watchdog: close after 30 min without client input
last_input = time.time()
IDLE_LIMIT = 1800
while True:
try:
packet = await asyncio.wait_for(ws.receive(), timeout=30.0)
except asyncio.TimeoutError:
if time.time() - last_input > IDLE_LIMIT:
await send_json({"type": "status", "message": "SSH idle timeout"})
break
# keepalive ping to browser
await send_json({"type": "pong"})
continue
if packet.get("type") == "websocket.disconnect":
break
text = packet.get("text")
if text is None:
continue
last_input = time.time()
if text.startswith("{") and '"type"' in text[:48]:
try:
ctrl = json.loads(text)
except Exception:
process.stdin.write(text)
await process.stdin.drain()
continue
ctype = ctrl.get("type")
if ctype == "resize":
try:
process.change_terminal_size(int(ctrl.get("cols") or 80), int(ctrl.get("rows") or 24))
except Exception:
pass
elif ctype == "data":
process.stdin.write(str(ctrl.get("data") or ""))
await process.stdin.drain()
elif ctype == "ping":
await send_json({"type": "pong"})
continue
process.stdin.write(text)
await process.stdin.drain()
except WebSocketDisconnect:
pass
except Exception as e:
log.warning("SSH websocket error: %s", e)
try:
await send_json({"type": "error", "message": str(e)})
except Exception:
pass
finally:
SSH_SESSIONS.pop(session_id, None)
if reader_task:
reader_task.cancel()
try:
await reader_task
except Exception:
pass
try:
if process:
process.close()
await asyncio.wait_for(process.wait_closed(), timeout=3.0)
except Exception:
pass
try:
if conn:
conn.close()
await asyncio.wait_for(conn.wait_closed(), timeout=3.0)
except Exception:
pass
if acquired:
try:
_get_ssh_sem().release()
except Exception:
pass
try:
await ws.close()
except Exception:
pass
log.info("SSH session end id=%s target=%s active=%s", session_id, client_host, len(SSH_SESSIONS))
# ---------------------------------------------------------------------------
# Reports / compliance / warranty / export (read-only OME surfaces)
# ---------------------------------------------------------------------------
async def fetch_warranties(force: bool = False) -> dict:
if not force:
cached = _cache_get("warranties")
if cached is not None:
return cached
rows: list[dict] = []
async with httpx.AsyncClient(verify=False, timeout=60.0) as client:
base, headers, sid = await ome_session(client)
try:
skip = 0
while True:
r = await client.get(
f"{base}/api/WarrantyService/Warranties",
headers=headers,
params={"$top": 200, "$skip": skip},
)
r.raise_for_status()
chunk = (r.json() or {}).get("value") or []
if not chunk:
break
for w in chunk:
rows.append(
{
"id": w.get("Id"),
"device_id": w.get("DeviceId"),
"service_tag": w.get("DeviceIdentifier"),
"device_name": w.get("DeviceName"),
"model": w.get("DeviceModel"),
"service_level": w.get("ServiceLevelDescription") or w.get("ServiceLevelCode"),
"start_date": w.get("StartDate"),
"end_date": w.get("EndDate"),
"days_remaining": w.get("DaysRemaining"),
"ship_date": w.get("SystemShipDate"),
"state": w.get("State"),
"country": w.get("CountryLookupCode"),
}
)
skip += len(chunk)
if len(chunk) < 200:
break
finally:
await ome_session_delete(client, base, headers, sid)
# Prefer longest-remaining warranty per device
by_device: dict[int, dict] = {}
for w in rows:
did = w.get("device_id")
if did is None:
continue
prev = by_device.get(did)
if not prev or int(w.get("days_remaining") or 0) > int(prev.get("days_remaining") or 0):
by_device[did] = w
elif int(w.get("days_remaining") or 0) == int(prev.get("days_remaining") or 0):
# keep later end date
if str(w.get("end_date") or "") > str(prev.get("end_date") or ""):
by_device[did] = w
payload = {
"updated_at": time.time(),
"count": len(rows),
"items": rows,
"by_device": {str(k): v for k, v in by_device.items()},
}
return _cache_set("warranties", payload)
async def fetch_baselines(force: bool = False) -> dict:
if not force:
cached = _cache_get("baselines")
if cached is not None:
return cached
async with httpx.AsyncClient(verify=False, timeout=60.0) as client:
base, headers, sid = await ome_session(client)
try:
r = await client.get(f"{base}/api/UpdateService/Baselines", headers=headers)
r.raise_for_status()
items = []
for b in (r.json() or {}).get("value") or []:
summary = b.get("ComplianceSummary") or {}
items.append(
{
"id": b.get("Id"),
"name": b.get("Name"),
"description": b.get("Description"),
"catalog_id": b.get("CatalogId"),
"catalog_type": b.get("CatalogType"),
"last_run": b.get("LastRun"),
"task_id": b.get("TaskId"),
"compliance_status": summary.get("ComplianceStatus"),
"critical": summary.get("NumberOfCritical"),
"warning": summary.get("NumberOfWarning"),
"normal": summary.get("NumberOfNormal"),
"downgrade": summary.get("NumberOfDowngrade"),
"unknown": summary.get("NumberOfUnknown"),
}
)
finally:
await ome_session_delete(client, base, headers, sid)
return _cache_set("baselines", {"updated_at": time.time(), "items": items})
async def fetch_catalogs(force: bool = False) -> dict:
if not force:
cached = _cache_get("catalogs")
if cached is not None:
return cached
async with httpx.AsyncClient(verify=False, timeout=60.0) as client:
base, headers, sid = await ome_session(client)
try:
r = await client.get(f"{base}/api/UpdateService/Catalogs", headers=headers)
r.raise_for_status()
items = []
for c in (r.json() or {}).get("value") or []:
repo = c.get("Repository") or {}
items.append(
{
"id": c.get("Id"),
"filename": c.get("Filename") or c.get("SourcePath"),
"source": repo.get("Source") or c.get("Source"),
"repository_name": repo.get("Name"),
"repository_type": repo.get("RepositoryType") or c.get("RepositoryType"),
"owner": c.get("Owner"),
"status": (c.get("Status") or {}).get("Name") if isinstance(c.get("Status"), dict) else c.get("Status"),
"last_updated": c.get("LastUpdated") or c.get("BundlesUpdateTime"),
}
)
finally:
await ome_session_delete(client, base, headers, sid)
return _cache_set("catalogs", {"updated_at": time.time(), "items": items})
def _pick_primary_baseline(baselines: list[dict]) -> dict | None:
if not baselines:
return None
for b in baselines:
name = (b.get("name") or "").lower()
if "dell" in name and "online" in name:
return b
# most critical first
return sorted(baselines, key=lambda x: int(x.get("critical") or 0), reverse=True)[0]
async def fetch_compliance(force: bool = False, baseline_id: int | None = None) -> dict:
if not force and baseline_id is None:
cached = _cache_get("compliance")
if cached is not None:
return cached
bl = await fetch_baselines(force=force)
primary = None
if baseline_id is not None:
primary = next((b for b in bl["items"] if b.get("id") == baseline_id), None)
if primary is None:
primary = _pick_primary_baseline(bl["items"])
if not primary:
payload = {
"updated_at": time.time(),
"summary": {"outdated_devices": 0, "critical_components": 0, "baseline_name": None},
"devices": [],
"components": [],
"baselines": bl["items"],
}
return _cache_set("compliance", payload)
bid = primary["id"]
devices_out: list[dict] = []
components_out: list[dict] = []
fleet_by_id = {d.get("id"): d for d in STATE.get("devices") or []}
async with httpx.AsyncClient(verify=False, timeout=120.0) as client:
base, headers, sid = await ome_session(client)
try:
r = await client.get(
f"{base}/api/UpdateService/Baselines({bid})/DeviceComplianceReports",
headers=headers,
)
r.raise_for_status()
for dcr in (r.json() or {}).get("value") or []:
did = dcr.get("DeviceId")
fleet = fleet_by_id.get(did) or {}
st = dcr.get("ServiceTag") or fleet.get("service_tag")
comps = dcr.get("ComponentComplianceReports") or []
device_row = {
"device_id": did,
"service_tag": st,
"name": fleet.get("name") or dcr.get("DeviceName") or st,
"model": dcr.get("DeviceModel") or fleet.get("model"),
"ip": fleet.get("ip"),
"firmware_status": dcr.get("FirmwareStatus"),
"compliance_status": dcr.get("ComplianceStatus"),
"reboot_required": dcr.get("RebootRequired"),
"component_count": len(comps),
"dell_uri": None,
}
devices_out.append(device_row)
for comp in comps:
uri = comp.get("Uri")
if uri and not device_row["dell_uri"]:
device_row["dell_uri"] = uri
components_out.append(
{
"device_id": did,
"service_tag": st,
"device_name": device_row["name"],
"model": device_row["model"],
"ip": device_row["ip"],
"component": comp.get("Name"),
"component_type": comp.get("ComponentType"),
"current_version": comp.get("CurrentVersion"),
"catalog_version": comp.get("Version"),
"update_action": comp.get("UpdateAction"),
"criticality": comp.get("Criticality"),
"compliance_status": comp.get("ComplianceStatus"),
"reboot_required": comp.get("RebootRequired"),
"dell_uri": uri,
"path": comp.get("Path"),
"baseline_id": bid,
"baseline_name": primary.get("name"),
"status_badge": (
"outdated"
if str(comp.get("UpdateAction") or "").upper() == "UPGRADE"
or str(comp.get("ComplianceStatus") or "").upper()
in ("CRITICAL", "WARNING", "NONCOMPLIANT", "NON-COMPLIANT")
else "current"
if str(comp.get("UpdateAction") or "").upper() in ("EQUAL", "NONE", "")
and str(comp.get("ComplianceStatus") or "").upper()
in ("", "OK", "COMPLIANT", "NORMAL", "DOWNGRADE")
else "unknown"
),
}
)
finally:
await ome_session_delete(client, base, headers, sid)
outdated = [
d
for d in devices_out
if str(d.get("compliance_status") or "").upper() in ("CRITICAL", "WARNING")
or str(d.get("firmware_status") or "").lower() in ("non-compliant", "noncompliant")
]
crit_comps = [
c
for c in components_out
if str(c.get("compliance_status") or "").upper() == "CRITICAL"
or str(c.get("update_action") or "").upper() == "UPGRADE"
]
payload = {
"updated_at": time.time(),
"baseline": primary,
"baselines": bl["items"],
"summary": {
"baseline_id": bid,
"baseline_name": primary.get("name"),
"outdated_devices": len(outdated),
"devices_in_report": len(devices_out),
"critical_components": len(crit_comps),
"components_total": len(components_out),
"compliance_status": primary.get("compliance_status"),
"last_run": primary.get("last_run"),
},
"devices": devices_out,
"components": components_out,
}
if baseline_id is None:
return _cache_set("compliance", payload)
return payload
async def fetch_report_defs(force: bool = False) -> dict:
if not force:
cached = _cache_get("report_defs")
if cached is not None:
return cached
items = []
async with httpx.AsyncClient(verify=False, timeout=60.0) as client:
base, headers, sid = await ome_session(client)
try:
r = await client.get(f"{base}/api/ReportService/ReportDefs", headers=headers)
r.raise_for_status()
for d in (r.json() or {}).get("value") or []:
cols = [c.get("Name") for c in (d.get("ColumnNames") or []) if c.get("Name")]
items.append(
{
"id": d.get("Id"),
"name": d.get("Name"),
"description": d.get("Description"),
"category": d.get("Category") or d.get("FilterGroupName"),
"is_builtin": d.get("IsBuiltIn"),
"last_run": d.get("LastRunDate"),
"last_run_by": d.get("LastRunBy"),
"columns": cols,
}
)
finally:
await ome_session_delete(client, base, headers, sid)
items.sort(key=lambda x: ((x.get("category") or ""), (x.get("name") or "").lower()))
return _cache_set("report_defs", {"updated_at": time.time(), "items": items})
async def fetch_jobs(force: bool = False, top: int = 40) -> dict:
if not force:
cached = _cache_get("jobs")
if cached is not None:
return cached
items = []
async with httpx.AsyncClient(verify=False, timeout=60.0) as client:
base, headers, sid = await ome_session(client)
try:
r = await client.get(
f"{base}/api/JobService/Jobs",
headers=headers,
params={"$top": top},
)
r.raise_for_status()
for j in (r.json() or {}).get("value") or []:
status = j.get("LastRunStatus") or {}
items.append(
{
"id": j.get("Id"),
"name": j.get("JobName") or j.get("Name"),
"status": status.get("Name") if isinstance(status, dict) else status,
"job_type": (j.get("JobType") or {}).get("Name")
if isinstance(j.get("JobType"), dict)
else j.get("JobType"),
"last_run": j.get("LastRunStatus") and (j.get("LastRunDate") or j.get("StartTime")),
"progress": j.get("Progress") or j.get("PercentComplete"),
"created_by": j.get("CreatedBy"),
}
)
finally:
await ome_session_delete(client, base, headers, sid)
return _cache_set("jobs", {"updated_at": time.time(), "items": items})
def build_fleet_report_rows(warranties: dict | None = None) -> list[dict]:
by_dev = (warranties or {}).get("by_device") or {}
rows = []
for d in STATE.get("devices") or []:
w = by_dev.get(str(d.get("id"))) or {}
rows.append(
{
"id": d.get("id"),
"name": d.get("name"),
"service_tag": d.get("service_tag"),
"model": d.get("model"),
"ip": d.get("ip"),
"subnet": d.get("subnet"),
"type": d.get("type"),
"sub_type": d.get("sub_type"),
"is_server": d.get("is_server"),
"is_idrac": d.get("is_idrac"),
"connected": d.get("connected"),
"powered_on": d.get("powered_on"),
"power_state": d.get("power_state"),
"status": d.get("status"),
"watts": d.get("watts"),
"avg_watts": d.get("avg_watts"),
"peak_watts": d.get("peak_watts"),
"last_status_time": d.get("last_status_time"),
"last_inventory_time": d.get("last_inventory_time"),
"warranty_end": w.get("end_date"),
"warranty_days_remaining": w.get("days_remaining"),
"warranty_service_level": w.get("service_level"),
}
)
rows.sort(key=lambda r: (r.get("name") or "").lower())
return rows
@app.get("/api/warranties")
async def api_warranties(force: bool = False):
return await fetch_warranties(force=force)
@app.get("/api/compliance")
async def api_compliance(force: bool = False, baseline_id: int | None = None):
return await fetch_compliance(force=force, baseline_id=baseline_id)
@app.get("/api/baselines")
async def api_baselines(force: bool = False):
return await fetch_baselines(force=force)
@app.get("/api/catalogs")
async def api_catalogs(force: bool = False):
return await fetch_catalogs(force=force)
@app.get("/api/ome/report-defs")
async def api_report_defs(force: bool = False):
return await fetch_report_defs(force=force)
@app.get("/api/ome/jobs")
async def api_ome_jobs(force: bool = False):
return await fetch_jobs(force=force)
class ReportRunIn(BaseModel):
report_def_id: int
@app.post("/api/ome/reports/run")
async def api_ome_report_run(payload: ReportRunIn):
async with httpx.AsyncClient(verify=False, timeout=60.0) as client:
base, headers, sid = await ome_session(client)
try:
r = await client.post(
f"{base}/api/ReportService/Actions/ReportService.RunReport",
headers=headers,
json={"ReportDefId": payload.report_def_id},
)
if r.status_code >= 400:
raise HTTPException(r.status_code, r.text[:500])
job_id = r.json() if isinstance(r.json(), (int, str)) else (r.json() or {}).get("Id") or r.text
return {
"job_id": job_id,
"report_def_id": payload.report_def_id,
"message": "Report job started in OME. Poll results shortly.",
"results_url": f"/api/ome/reports/{payload.report_def_id}/results",
}
finally:
await ome_session_delete(client, base, headers, sid)
@app.get("/api/ome/reports/{report_def_id}/results")
async def api_ome_report_results(report_def_id: int):
async with httpx.AsyncClient(verify=False, timeout=60.0) as client:
base, headers, sid = await ome_session(client)
try:
r = await client.get(
f"{base}/api/ReportService/ReportDefs({report_def_id})/ReportResults",
headers=headers,
)
if r.status_code >= 400:
raise HTTPException(
r.status_code,
(r.json().get("error", {}) or {}).get("message")
if r.headers.get("content-type", "").startswith("application/json")
else r.text[:500],
)
return r.json()
finally:
await ome_session_delete(client, base, headers, sid)
@app.get("/api/reports/fleet")
async def api_reports_fleet(force: bool = False):
warranties = await fetch_warranties(force=force)
rows = build_fleet_report_rows(warranties)
return {
"updated_at": STATE.get("updated_at"),
"summary": STATE.get("summary"),
"ome": STATE.get("ome"),
"count": len(rows),
"rows": rows,
}
@app.get("/api/reports/firmware")
async def api_reports_firmware(force: bool = False, baseline_id: int | None = None):
return await fetch_compliance(force=force, baseline_id=baseline_id)
@app.get("/api/reports/brief")
async def api_reports_brief(force: bool = False):
warranties = await fetch_warranties(force=force)
compliance = await fetch_compliance(force=force)
fleet_rows = build_fleet_report_rows(warranties)
critical = [a for a in (STATE.get("alerts") or []) if a.get("severity") == "Critical"][:15]
expiring = sorted(
[
w
for w in (warranties.get("items") or [])
if w.get("days_remaining") is not None and int(w.get("days_remaining") or 0) <= 90
],
key=lambda x: int(x.get("days_remaining") or 0),
)[:25]
outdated = [
d
for d in (compliance.get("devices") or [])
if str(d.get("compliance_status") or "").upper() in ("CRITICAL", "WARNING")
or str(d.get("firmware_status") or "").lower() in ("non-compliant", "noncompliant")
]
return {
"generated_at": time.time(),
"ome": STATE.get("ome"),
"summary": STATE.get("summary"),
"compliance_summary": compliance.get("summary"),
"critical_alerts": critical,
"outdated_devices": outdated[:40],
"warranty_expiring": expiring,
"fleet": fleet_rows,
"baseline": compliance.get("baseline"),
}
@app.get("/api/export/{kind}")
async def api_export(kind: str, fmt: str = "csv", force: bool = False, baseline_id: int | None = None):
kind = kind.lower().strip()
fmt = fmt.lower().strip()
if fmt not in ("csv", "json"):
raise HTTPException(400, "fmt must be csv or json")
if kind in ("fleet", "inventory"):
data = await api_reports_fleet(force=force)
rows = data["rows"]
filename = f"ome-fleet-{int(time.time())}"
elif kind in ("firmware", "compliance"):
data = await fetch_compliance(force=force, baseline_id=baseline_id)
rows = data.get("components") or []
filename = f"ome-firmware-compliance-{int(time.time())}"
elif kind == "warranty":
data = await fetch_warranties(force=force)
rows = data.get("items") or []
filename = f"ome-warranties-{int(time.time())}"
elif kind == "brief":
data = await api_reports_brief(force=force)
if fmt == "json":
return Response(
content=json.dumps(data, indent=2, default=str),
media_type="application/json",
headers={"Content-Disposition": f'attachment; filename="ome-customer-brief-{int(time.time())}.json"'},
)
# flatten brief as fleet CSV appendix
rows = data.get("fleet") or []
filename = f"ome-customer-brief-fleet-{int(time.time())}"
else:
raise HTTPException(404, "Unknown export kind. Use fleet|firmware|warranty|brief")
if fmt == "json":
body = json.dumps({"kind": kind, "count": len(rows), "rows": rows}, indent=2, default=str)
return Response(
content=body,
media_type="application/json",
headers={"Content-Disposition": f'attachment; filename="{filename}.json"'},
)
csv_body = rows_to_csv(rows)
return Response(
content=csv_body,
media_type="text/csv",
headers={"Content-Disposition": f'attachment; filename="{filename}.csv"'},
)
@app.get("/api/devices/{device_id}/warranty")
async def device_warranty(device_id: int, force: bool = False):
warranties = await fetch_warranties(force=force)
items = [w for w in warranties.get("items") or [] if w.get("device_id") == device_id]
primary = (warranties.get("by_device") or {}).get(str(device_id))
return {"device_id": device_id, "primary": primary, "items": items}
@app.get("/api/devices/{device_id}/compliance")
async def device_compliance(device_id: int, force: bool = False):
compliance = await fetch_compliance(force=force)
device = next((d for d in compliance.get("devices") or [] if d.get("device_id") == device_id), None)
comps = [c for c in compliance.get("components") or [] if c.get("device_id") == device_id]
return {
"device_id": device_id,
"baseline": compliance.get("baseline"),
"device": device,
"components": comps,
}
@app.get("/api/ssh/sessions")
async def api_ssh_sessions():
"""Ops visibility: active SSH bridges (no secrets)."""
now = time.time()
sessions = [
{
"id": sid,
"client": meta.get("client"),
"host": meta.get("host"),
"user": meta.get("user"),
"age_sec": int(now - float(meta.get("started") or now)),
}
for sid, meta in list(SSH_SESSIONS.items())
]
return {"active": len(sessions), "max": SSH_MAX_SESSIONS, "sessions": sessions}
STATIC_DIR = Path("/ui")
@app.get("/")
async def index():
resp = FileResponse(STATIC_DIR / "index.html")
resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
resp.headers["Pragma"] = "no-cache"
return resp
@app.get("/styles.css")
async def styles():
return FileResponse(STATIC_DIR / "styles.css", media_type="text/css")
@app.get("/app.js")
async def app_js():
return FileResponse(STATIC_DIR / "app.js", media_type="application/javascript")
@app.get("/ops.js")
async def ops_js():
return FileResponse(STATIC_DIR / "ops.js", media_type="application/javascript")
@app.get("/ssh.js")
async def ssh_js():
return FileResponse(STATIC_DIR / "ssh.js", media_type="application/javascript")
@app.get("/vendor/xterm/xterm.css")
async def vendor_xterm_css():
return FileResponse(STATIC_DIR / "vendor/xterm/xterm.css", media_type="text/css")
@app.get("/vendor/xterm/xterm.min.js")
async def vendor_xterm_js():
return FileResponse(
STATIC_DIR / "vendor/xterm/xterm.min.js",
media_type="application/javascript",
)
@app.get("/vendor/xterm/xterm-addon-fit.min.js")
async def vendor_xterm_fit_js():
return FileResponse(
STATIC_DIR / "vendor/xterm/xterm-addon-fit.min.js",
media_type="application/javascript",
)
@app.get("/reports.js")
async def reports_js():
return FileResponse(STATIC_DIR / "reports.js", media_type="application/javascript")
@app.get("/dell.png")
async def dell_png():
return FileResponse(STATIC_DIR / "dell.png", media_type="image/png")
@app.get("/dell-mark.png")
async def dell_mark():
return FileResponse(STATIC_DIR / "dell-mark.png", media_type="image/png")
@app.get("/dell.svg")
async def dell_svg():
return FileResponse(STATIC_DIR / "dell.png", media_type="image/png")