Files
ome-cockpit/api/main.py
T
root 1e87f22006 Add VLAN inventory, Present decks, and network fabric mapping.
Seed all ATC/FDE VLANs with live host inventory and editable notes, fix cluster membership on the map, and ship Present/network UI polish.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-18 21:56:30 +02:00

6095 lines
242 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.
import asyncio
import json
import re
import os
import socket
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 = 1100
chat_system_chars: int = 12000
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"
# ---------------------------------------------------------------------------
# Dell PowerEdge expansion / memory population catalog (from Dell Tech Guides
# + Installation & Service Manuals / KB 000141539). Used for empty-slot maps,
# max capacity, and best-practice guidance — not a live scrape.
# ---------------------------------------------------------------------------
_DELL_LINKS = {
# Verified live PDFs / product docs (checked 2026-07-17: %PDF or HTTP 200 HTML).
"spec_r740xd": "https://i.dell.com/sites/csdocuments/Product_Docs/en/poweredge-r740xd-spec-sheet.pdf",
"spec_r740": "https://i.dell.com/sites/csdocuments/Product_Docs/en/poweredge-r740-spec-sheet.pdf",
"tech_r740": "https://i.dell.com/sites/csdocuments/Shared-Content_data-Sheets_Documents/en/aa/PowerEdge_R740_R740xd_Technical_Guide.pdf",
"own_r740xd": "https://dl.dell.com/topicspdf/poweredge-r740xd_owners-manual_en-us.pdf",
"docs_r740xd": "https://www.dell.com/support/product-details/en-us/product/poweredge-r740xd/docs",
"docs_r740": "https://www.dell.com/support/product-details/en-us/product/poweredge-r740/docs",
"spec_r640": "https://i.dell.com/sites/csdocuments/Product_Docs/en/poweredge-r640-spec-sheet.pdf",
"own_r640": "https://dl.dell.com/topicspdf/poweredge-r640_owners-manual_en-us.pdf",
"docs_r640": "https://www.dell.com/support/product-details/en-us/product/poweredge-r640/docs",
"spec_r440": "https://i.dell.com/sites/csdocuments/Product_Docs/en/poweredge-r440-spec-sheet.pdf",
"docs_r440": "https://www.dell.com/support/product-details/en-us/product/poweredge-r440/docs",
"spec_r6415": "https://i.dell.com/sites/csdocuments/Shared-Content_data-Sheets_Documents/en/aa/PowerEdge_R6415_Spec_Sheet_EN.pdf",
"ref_r6415": "https://dl.dell.com/topicspdf/poweredge-r6415_reference-guide_en-us.pdf",
"own_r6415": "https://dl.dell.com/topicspdf/poweredge-r6415_owners-manual_en-us.pdf",
"docs_r6415": "https://www.dell.com/support/product-details/en-us/product/poweredge-r6415/docs",
"spec_r7425": "https://i.dell.com/sites/csdocuments/Shared-Content_data-Sheets_Documents/en/aa/PowerEdge-R7425-Spec-Sheet.pdf",
"own_r7425": "https://dl.dell.com/topicspdf/poweredge-r7425_owners-manual_en-us.pdf",
"docs_r7425": "https://www.dell.com/support/product-details/en-us/product/poweredge-r7425/docs",
"spec_r660": "https://www.delltechnologies.com/asset/en-us/products/servers/technical-support/poweredge-r660-spec-sheet.pdf",
"tech_r660": "https://www.delltechnologies.com/asset/en-us/products/servers/technical-support/poweredge-r660-technical-guide.pdf",
"docs_r660": "https://www.dell.com/support/product-details/en-us/product/poweredge-r660/docs",
"spec_xr5610": "https://www.delltechnologies.com/asset/en-us/products/servers/technical-support/poweredge-xr5610-spec-sheet.pdf",
"tech_xr5610": "https://www.delltechnologies.com/asset/en-us/products/servers/technical-support/poweredge-xr5610-technical-guide.pdf",
"docs_xr5610": "https://www.dell.com/support/product-details/en-us/product/poweredge-xr5610/docs",
"support_home": "https://www.dell.com/support/home",
}
# 14G dual-socket channel map (R640/R740/R740xd family)
_CH14 = {
"A": {1: 0, 7: 0, 2: 1, 8: 1, 3: 2, 9: 2, 4: 3, 10: 3, 5: 4, 11: 4, 6: 5, 12: 5},
"B": {1: 0, 7: 0, 2: 1, 8: 1, 3: 2, 9: 2, 4: 3, 10: 3, 5: 4, 11: 4, 6: 5, 12: 5},
}
# Dell datacenter rack catalog — visual U heights + face styles for rack designer
DELL_RACK_CATALOG: list[dict[str, Any]] = [
# --- PowerEdge 1U ---
{"sku": "PE-R440", "name": "PowerEdge R440", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r440"]},
{"sku": "PE-R640", "name": "PowerEdge R640", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r640"]},
{"sku": "PE-R650", "name": "PowerEdge R650", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r650"]},
{"sku": "PE-R650xs", "name": "PowerEdge R650xs", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r650xs"]},
{"sku": "PE-R660", "name": "PowerEdge R660", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r660"]},
{"sku": "PE-R660xs", "name": "PowerEdge R660xs", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r660xs"]},
{"sku": "PE-R6615", "name": "PowerEdge R6615", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r6615"]},
{"sku": "PE-R6415", "name": "PowerEdge R6415", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r6415"]},
{"sku": "PE-R6515", "name": "PowerEdge R6515", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r6515"]},
{"sku": "PE-R6525", "name": "PowerEdge R6525", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r6525"]},
{"sku": "PE-R6715", "name": "PowerEdge R6715", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r6715"]},
{"sku": "PE-R670", "name": "PowerEdge R670", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r670"]},
{"sku": "PE-XR5610", "name": "PowerEdge XR5610", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["xr5610"]},
{"sku": "PE-C6520", "name": "PowerEdge C6520", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["c6520"]},
{"sku": "PE-C6525", "name": "PowerEdge C6525", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["c6525"]},
{"sku": "PE-C4140", "name": "PowerEdge C4140", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["c4140"]},
# --- PowerEdge 2U ---
{"sku": "PE-R740", "name": "PowerEdge R740", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r740"]},
{"sku": "PE-R740xd", "name": "PowerEdge R740xd", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r740xd"]},
{"sku": "PE-R740xd2", "name": "PowerEdge R740xd2", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r740xd2"]},
{"sku": "PE-R7425", "name": "PowerEdge R7425", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r7425"]},
{"sku": "PE-R750", "name": "PowerEdge R750", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r750"]},
{"sku": "PE-R750xa", "name": "PowerEdge R750xa", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r750xa"]},
{"sku": "PE-R7515", "name": "PowerEdge R7515", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r7515"]},
{"sku": "PE-R7525", "name": "PowerEdge R7525", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r7525"]},
{"sku": "PE-R760", "name": "PowerEdge R760", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r760"]},
{"sku": "PE-R760xd2", "name": "PowerEdge R760xd2", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r760xd2"]},
{"sku": "PE-R760xa", "name": "PowerEdge R760xa", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r760xa"]},
{"sku": "PE-R770", "name": "PowerEdge R770", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r770"]},
{"sku": "PE-R840", "name": "PowerEdge R840", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r840"]},
{"sku": "PE-R940", "name": "PowerEdge R940", "family": "PowerEdge", "category": "server", "u_height": 3, "face": "server_4u", "match": ["r940"]},
{"sku": "PE-R940xa", "name": "PowerEdge R940xa", "family": "PowerEdge", "category": "server", "u_height": 4, "face": "server_4u", "match": ["r940xa"]},
{"sku": "PE-R340", "name": "PowerEdge R340", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r340"]},
{"sku": "PE-R450", "name": "PowerEdge R450", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r450"]},
{"sku": "PE-R550", "name": "PowerEdge R550", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r550"]},
{"sku": "PE-T640", "name": "PowerEdge T640", "family": "PowerEdge", "category": "server", "u_height": 5, "face": "server_4u", "match": ["t640"]},
{"sku": "PE-DSS8440", "name": "DSS8440", "family": "PowerEdge", "category": "server", "u_height": 4, "face": "server_4u", "match": ["dss8440"]},
{"sku": "PE-XE9680", "name": "PowerEdge XE9680", "family": "PowerEdge", "category": "server", "u_height": 6, "face": "server_4u", "match": ["xe9680"]},
{"sku": "PE-XE8640", "name": "PowerEdge XE8640", "family": "PowerEdge", "category": "server", "u_height": 4, "face": "server_4u", "match": ["xe8640"]},
# --- MX / modular ---
{"sku": "PE-MX7000", "name": "PowerEdge MX7000", "family": "PowerEdge MX", "category": "chassis", "u_height": 7, "face": "chassis_mx", "match": ["mx7000"]},
{"sku": "PE-MX740c", "name": "PowerEdge MX740c", "family": "PowerEdge MX", "category": "blade", "u_height": 0, "face": "blade", "match": ["mx740c"]},
{"sku": "PE-MX750c", "name": "PowerEdge MX750c", "family": "PowerEdge MX", "category": "blade", "u_height": 0, "face": "blade", "match": ["mx750c"]},
{"sku": "PE-M640", "name": "PowerEdge M640", "family": "PowerEdge", "category": "blade", "u_height": 0, "face": "blade", "match": ["m640"]},
# --- Networking / PowerSwitch / Force10 ---
{"sku": "PS-S4048", "name": "PowerSwitch S4048-ON", "family": "PowerSwitch", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["s4048", "force10"]},
{"sku": "PS-S4148", "name": "PowerSwitch S4148-ON", "family": "PowerSwitch", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["s4148"]},
{"sku": "PS-S5248", "name": "PowerSwitch S5248F-ON", "family": "PowerSwitch", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["s5248"]},
{"sku": "PS-S5296", "name": "PowerSwitch S5296F-ON", "family": "PowerSwitch", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["s5296"]},
{"sku": "PS-N3248", "name": "PowerSwitch N3248TE-ON", "family": "PowerSwitch", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["n3248"]},
{"sku": "PS-Z9264", "name": "PowerSwitch Z9264F-ON", "family": "PowerSwitch", "category": "switch", "u_height": 2, "face": "switch_2u", "match": ["z9264"]},
{"sku": "PS-Z9432", "name": "PowerSwitch Z9432F-ON", "family": "PowerSwitch", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["z9432"]},
{"sku": "PC-5548", "name": "PowerConnect 5548", "family": "PowerConnect", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["powerconnect 5548", "pc5548"]},
{"sku": "PC-6248", "name": "PowerConnect 6248", "family": "PowerConnect", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["powerconnect 6248", "pc6248"]},
{"sku": "PC-7048", "name": "PowerConnect 7048", "family": "PowerConnect", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["powerconnect 7048", "pc7048"]},
{"sku": "PC-8024", "name": "PowerConnect 8024F", "family": "PowerConnect", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["powerconnect 8024", "pc8024"]},
{"sku": "PC-8132", "name": "PowerConnect 8132F", "family": "PowerConnect", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["powerconnect 8132", "pc8132"]},
{"sku": "MX-9116n", "name": "MX9116n Fabric Switching Engine", "family": "PowerEdge MX", "category": "switch", "u_height": 0, "face": "fabric", "match": ["mx9116"]},
{"sku": "MX-5108n", "name": "MX5108n Ethernet Switch", "family": "PowerEdge MX", "category": "switch", "u_height": 0, "face": "fabric", "match": ["mx5108"]},
# --- Storage ---
{"sku": "ME-4012", "name": "PowerVault ME4012", "family": "PowerVault", "category": "storage", "u_height": 2, "face": "storage_2u", "match": ["me4012"]},
{"sku": "ME-4024", "name": "PowerVault ME4024", "family": "PowerVault", "category": "storage", "u_height": 2, "face": "storage_2u", "match": ["me4024"]},
{"sku": "ME-5012", "name": "PowerVault ME5012", "family": "PowerVault", "category": "storage", "u_height": 2, "face": "storage_2u", "match": ["me5012"]},
{"sku": "ME-5024", "name": "PowerVault ME5024", "family": "PowerVault", "category": "storage", "u_height": 2, "face": "storage_2u", "match": ["me5024"]},
{"sku": "SC-AllFlash", "name": "SC Series Array", "family": "PowerStore/SC", "category": "storage", "u_height": 3, "face": "storage_3u", "match": ["sc "]},
{"sku": "PST-5000T", "name": "PowerStore 5000T", "family": "PowerStore/SC", "category": "storage", "u_height": 2, "face": "storage_2u", "match": ["powerstore 5000"]},
{"sku": "PST-1200T", "name": "PowerStore 1200T", "family": "PowerStore/SC", "category": "storage", "u_height": 2, "face": "storage_2u", "match": ["powerstore 1200"]},
{"sku": "PST-3200T", "name": "PowerStore 3200T", "family": "PowerStore/SC", "category": "storage", "u_height": 2, "face": "storage_2u", "match": ["powerstore 3200"]},
{"sku": "DD-6900", "name": "PowerProtect DD6900", "family": "PowerProtect", "category": "storage", "u_height": 4, "face": "storage_3u", "match": ["dd6900"]},
{"sku": "DD-9400", "name": "PowerProtect DD9400", "family": "PowerProtect", "category": "storage", "u_height": 4, "face": "storage_3u", "match": ["dd9400"]},
{"sku": "UNITY-380", "name": "Unity XT 380", "family": "Unity", "category": "storage", "u_height": 2, "face": "storage_2u", "match": ["unity"]},
# --- VxRail appliances ---
{"sku": "VX-E460F", "name": "VxRail E460F", "family": "VxRail", "category": "server", "u_height": 1, "face": "server_1u", "match": ["e460f", "vxrail e"]},
{"sku": "VX-V470F", "name": "VxRail V470F", "family": "VxRail", "category": "server", "u_height": 1, "face": "server_1u", "match": ["v470f"]},
{"sku": "VX-P570F", "name": "VxRail P570F", "family": "VxRail", "category": "server", "u_height": 1, "face": "server_1u", "match": ["p570f"]},
{"sku": "VX-V670F", "name": "VxRail V670F", "family": "VxRail", "category": "server", "u_height": 2, "face": "server_2u", "match": ["v670f"]},
{"sku": "VX-P670F", "name": "VxRail P670F", "family": "VxRail", "category": "server", "u_height": 2, "face": "server_2u", "match": ["p670f"]},
{"sku": "VX-S570", "name": "VxRail S570", "family": "VxRail", "category": "server", "u_height": 1, "face": "server_1u", "match": ["s570"]},
# --- Networking extras ---
{"sku": "PS-S5232", "name": "PowerSwitch S5232F-ON", "family": "PowerSwitch", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["s5232"]},
{"sku": "PS-S5212", "name": "PowerSwitch S5212F-ON", "family": "PowerSwitch", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["s5212"]},
{"sku": "PS-N3224", "name": "PowerSwitch N3224T-ON", "family": "PowerSwitch", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["n3224"]},
{"sku": "PS-Z9100", "name": "PowerSwitch Z9100-ON", "family": "PowerSwitch", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["z9100"]},
{"sku": "PC-7024", "name": "PowerConnect 7024", "family": "PowerConnect", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["powerconnect 7024"]},
{"sku": "PC-8100", "name": "PowerConnect 8100", "family": "PowerConnect", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["powerconnect 8100"]},
# --- PDU / misc ---
{"sku": "PDU-Geist-0U", "name": "Geist Rack PDU (0U vertical)", "family": "PDU", "category": "pdu", "u_height": 0, "face": "pdu_0u", "match": ["geist", "mg03", "mgs3"]},
{"sku": "PDU-1U", "name": "Rack PDU 1U horizontal", "family": "PDU", "category": "pdu", "u_height": 1, "face": "pdu_1u", "match": []},
{"sku": "PDU-APC-0U", "name": "APC Rack PDU 0U", "family": "PDU", "category": "pdu", "u_height": 0, "face": "pdu_0u", "match": ["apc"]},
{"sku": "PANEL-1U", "name": "Blanking panel 1U", "family": "Accessory", "category": "blank", "u_height": 1, "face": "blank_1u", "match": []},
{"sku": "PANEL-2U", "name": "Blanking panel 2U", "family": "Accessory", "category": "blank", "u_height": 2, "face": "blank_2u", "match": []},
{"sku": "CABLE-MGMT-1U", "name": "Cable management arm 1U", "family": "Accessory", "category": "blank", "u_height": 1, "face": "blank_1u", "match": []},
{"sku": "KVM-1U", "name": "Rack KVM 1U", "family": "Accessory", "category": "custom", "u_height": 1, "face": "custom", "match": ["kvm"]},
{"sku": "CUSTOM", "name": "Custom device", "family": "Custom", "category": "custom", "u_height": 1, "face": "custom", "match": []},
]
FACE_IMAGE_MAP: dict[str, str] = {
"server_1u": "/assets/dell/face-server-1u.jpg?v=bezel2",
"server_2u": "/assets/dell/face-server-2u.jpg?v=bezel2",
"server_4u": "/assets/dell/face-server-4u.jpg?v=bezel2",
"switch_1u": "/assets/dell/face-switch-1u.jpg?v=bezel2",
"switch_2u": "/assets/dell/face-switch-1u.jpg?v=bezel2",
"storage_2u": "/assets/dell/face-storage-2u.jpg?v=bezel2",
"storage_3u": "/assets/dell/face-storage-2u.jpg?v=bezel2",
"chassis_mx": "/assets/dell/face-chassis-mx.jpg?v=bezel2",
"pdu_1u": "/assets/dell/face-pdu-1u.jpg?v=bezel2",
"pdu_0u": "/assets/dell/face-pdu-1u.jpg?v=bezel2",
"blank_1u": "/assets/dell/face-blank-1u.jpg?v=bezel2",
"blank_2u": "/assets/dell/face-blank-1u.jpg?v=bezel2",
"blade": "/assets/dell/face-chassis-mx.jpg?v=bezel2",
"fabric": "/assets/dell/face-switch-1u.jpg?v=bezel2",
"custom": "/assets/dell/face-server-1u.jpg?v=bezel2",
}
# Official Dell product shots by SKU (processed to dark background to match faces)
SKU_IMAGE_MAP: dict[str, str] = {
# Prefer shared dark face art for visual consistency across all PowerEdge 1U/2U
}
def catalog_image_for(item: dict) -> str:
sku = item.get("sku") or ""
if sku in SKU_IMAGE_MAP:
return SKU_IMAGE_MAP[sku]
face = item.get("face") or "custom"
return FACE_IMAGE_MAP.get(face, FACE_IMAGE_MAP["custom"])
def enrich_catalog_images() -> None:
for item in DELL_RACK_CATALOG:
item["image"] = catalog_image_for(item)
enrich_catalog_images()
def match_rack_catalog(model: str | None, role: str | None = None) -> dict | None:
m = (model or "").lower()
best = None
for item in DELL_RACK_CATALOG:
for token in item.get("match") or []:
if token and token in m:
best = item
break
if best:
break
if best:
return best
if role == "switch":
return next((x for x in DELL_RACK_CATALOG if x["sku"] == "PS-S4048"), None)
if role == "storage":
return next((x for x in DELL_RACK_CATALOG if x["sku"] == "ME-4024"), None)
if role == "chassis":
return next((x for x in DELL_RACK_CATALOG if x["sku"] == "PE-MX7000"), None)
if role == "pdu":
return next((x for x in DELL_RACK_CATALOG if x["sku"] == "PDU-Geist-0U"), None)
if role == "server":
return next((x for x in DELL_RACK_CATALOG if x["sku"] == "PE-R740"), None)
return None
MODEL_EXPANSION: dict[str, dict[str, Any]] = {
"PowerEdge R740xd": {
"generation": "14G",
"dimm_slots": 24,
"dimms_per_cpu": 12,
"channels_per_cpu": 6,
"max_memory_tb": 3.0,
"max_memory_note": "Up to 3 TB with LRDIMM (+ DCPMM configs); typical RDIMM max lower per CPU SKU",
"memory_types": ["DDR4 RDIMM", "DDR4 LRDIMM", "NVDIMM-N", "DCPMM/PMem"],
"max_dimm_speed_mts": 2666,
"pcie_slots_max": 8,
"pcie_note": "Up to 8× PCIe 3.0 via riser options; slot count depends on riser kit + CPU count",
"drive_bays_max": 24,
"drive_note": "Chassis-dependent: up to 24× 2.5\" (or mixed 3.5\"/NVMe mid-tray options on xd)",
"cpu_sockets": 2,
"best_practices": [
"Populate identical DIMMs across all six channels per CPU (6 or 12 DIMMs/CPU) for best bandwidth.",
"White release-tab sockets first, then black (channel rank 1 then rank 2).",
"Do not mix RDIMM and LRDIMM in the same system.",
"Unbalanced channel population reduces memory bandwidth / can lower effective speed.",
"Performance Optimized (4 DIMMs/CPU): slots 1,2,4,5 — (8 DIMMs/CPU): 1,2,4,5,7,8,10,11 (Dell KB 000141539).",
],
"performance_notes": [
"Peak memory bandwidth needs one matched DIMM in every channel (6 per CPU) before adding the second rank (slots 712).",
"Mixing capacities: place largest DIMMs in the first populated sockets of each channel.",
"DCPMM: up to 6 PMem + 6 DRAM per CPU; full 12-slot population recommended for App Direct / Memory Mode.",
],
"docs": [
{"title": "R740xd Spec Sheet (PDF)", "url": _DELL_LINKS["spec_r740xd"], "kind": "spec"},
{"title": "R740/R740xd Technical Guide (PDF)", "url": _DELL_LINKS["tech_r740"], "kind": "tech"},
{"title": "R740xd Owner's Manual (PDF)", "url": _DELL_LINKS["own_r740xd"], "kind": "manual"},
{"title": "Dell Support · R740xd docs", "url": _DELL_LINKS["docs_r740xd"], "kind": "support"},
],
"channel_map": _CH14,
"slot_labels": [f"A{i}" for i in range(1, 13)] + [f"B{i}" for i in range(1, 13)],
},
"PowerEdge R740": {
"generation": "14G",
"dimm_slots": 24,
"dimms_per_cpu": 12,
"channels_per_cpu": 6,
"max_memory_tb": 3.0,
"max_memory_note": "Up to 3 TB with LRDIMM/DCPMM depending on CPU",
"memory_types": ["DDR4 RDIMM", "DDR4 LRDIMM", "NVDIMM-N", "DCPMM/PMem"],
"max_dimm_speed_mts": 2666,
"pcie_slots_max": 8,
"pcie_note": "Up to 8× PCIe 3.0 (riser dependent)",
"drive_bays_max": 16,
"drive_note": "Up to 16× 2.5\" or 8× 3.5\" depending on chassis kit",
"cpu_sockets": 2,
"best_practices": [
"Same 14G rules as R740xd: balance channels; white tabs before black.",
"Performance Optimized 4/8 DIMM/CPU populations per Dell KB 000141539.",
],
"performance_notes": [
"Populate 6 identical DIMMs per CPU before doubling up channels for max throughput.",
],
"docs": [
{"title": "R740 Spec Sheet (PDF)", "url": _DELL_LINKS["spec_r740"], "kind": "spec"},
{"title": "R740/R740xd Technical Guide (PDF)", "url": _DELL_LINKS["tech_r740"], "kind": "tech"},
{"title": "Dell Support · R740 docs", "url": _DELL_LINKS["docs_r740"], "kind": "support"},
],
"channel_map": _CH14,
"slot_labels": [f"A{i}" for i in range(1, 13)] + [f"B{i}" for i in range(1, 13)],
},
"PowerEdge R640": {
"generation": "14G",
"dimm_slots": 24,
"dimms_per_cpu": 12,
"channels_per_cpu": 6,
"max_memory_tb": 3.0,
"memory_types": ["DDR4 RDIMM", "DDR4 LRDIMM", "DCPMM/PMem"],
"max_dimm_speed_mts": 2666,
"pcie_slots_max": 3,
"pcie_note": "1U: up to 3× PCIe 3.0 (+ rNDC)",
"drive_bays_max": 10,
"drive_note": "Up to 10× 2.5\" (NVMe options vary)",
"cpu_sockets": 2,
"best_practices": [
"14G dual-socket population: identical DIMMs across 6 channels per CPU.",
"White-tab sockets first; avoid RDIMM+LRDIMM mix.",
],
"performance_notes": [
"1U thermal/airflow limits denser GPU/NVMe kits — verify riser/backplane BOM.",
],
"docs": [
{"title": "R640 Spec Sheet (PDF)", "url": _DELL_LINKS["spec_r640"], "kind": "spec"},
{"title": "R640 Owner's Manual (PDF)", "url": _DELL_LINKS["own_r640"], "kind": "manual"},
{"title": "Dell Support · R640 docs", "url": _DELL_LINKS["docs_r640"], "kind": "support"},
],
"channel_map": _CH14,
"slot_labels": [f"A{i}" for i in range(1, 13)] + [f"B{i}" for i in range(1, 13)],
},
"PowerEdge R440": {
"generation": "14G",
"dimm_slots": 16,
"dimms_per_cpu": 8,
"channels_per_cpu": 6,
"max_memory_tb": 0.512,
"memory_types": ["DDR4 RDIMM"],
"max_dimm_speed_mts": 2666,
"pcie_slots_max": 3,
"drive_bays_max": 10,
"cpu_sockets": 2,
"best_practices": ["Populate channels evenly; match DIMM size/speed/rank per Dell 14G guidance."],
"performance_notes": ["Entry 14G — keep DIMM speeds matched to CPU QPI/UPI memory controller limits."],
"docs": [
{"title": "R440 Spec Sheet (PDF)", "url": _DELL_LINKS["spec_r440"], "kind": "spec"},
{"title": "Dell Support · R440 docs", "url": _DELL_LINKS["docs_r440"], "kind": "support"},
],
"slot_labels": [f"A{i}" for i in range(1, 9)] + [f"B{i}" for i in range(1, 9)],
},
"PowerEdge R6415": {
"generation": "14G-AMD",
"dimm_slots": 16,
"dimms_per_cpu": 16,
"channels_per_cpu": 8,
"max_memory_tb": 1.0,
"memory_types": ["DDR4 RDIMM", "DDR4 LRDIMM"],
"max_dimm_speed_mts": 2666,
"pcie_slots_max": 3,
"drive_bays_max": 10,
"cpu_sockets": 1,
"best_practices": [
"AMD EPYC Naples/Rome 14G: for 4 DIMMs use slots 1,3,5,7 (Dell KB 000141539).",
"Populate evenly across memory channels for Infinity Fabric bandwidth.",
],
"performance_notes": [
"EPYC memory performance scales strongly with channels populated — prefer 8 identical DIMMs when possible.",
],
"docs": [
{"title": "R6415 Spec Sheet (PDF)", "url": _DELL_LINKS["spec_r6415"], "kind": "spec"},
{"title": "R6415 Tech Specs / Reference (PDF)", "url": _DELL_LINKS["ref_r6415"], "kind": "tech"},
{"title": "R6415 Owner's Manual (PDF)", "url": _DELL_LINKS["own_r6415"], "kind": "manual"},
{"title": "Dell Support · R6415 docs", "url": _DELL_LINKS["docs_r6415"], "kind": "support"},
],
"slot_labels": [f"A{i}" for i in range(1, 17)],
},
"PowerEdge R7425": {
"generation": "14G-AMD",
"dimm_slots": 32,
"dimms_per_cpu": 16,
"channels_per_cpu": 8,
"max_memory_tb": 2.0,
"memory_types": ["DDR4 RDIMM", "DDR4 LRDIMM"],
"max_dimm_speed_mts": 2666,
"pcie_slots_max": 8,
"drive_bays_max": 24,
"cpu_sockets": 2,
"best_practices": [
"Dual EPYC: mirror CPU1 and CPU2 DIMM populations.",
"4 DIMMs/CPU → slots 1,3,5,7 per Dell AMD 14G guidance.",
],
"performance_notes": ["Balance both sockets identically to avoid NUMA imbalance."],
"docs": [
{"title": "R7425 Spec Sheet (PDF)", "url": _DELL_LINKS["spec_r7425"], "kind": "spec"},
{"title": "R7425 Owner's Manual (PDF)", "url": _DELL_LINKS["own_r7425"], "kind": "manual"},
{"title": "Dell Support · R7425 docs", "url": _DELL_LINKS["docs_r7425"], "kind": "support"},
],
"slot_labels": [f"A{i}" for i in range(1, 17)] + [f"B{i}" for i in range(1, 17)],
},
"PowerEdge R660": {
"generation": "16G",
"dimm_slots": 32,
"dimms_per_cpu": 16,
"channels_per_cpu": 8,
"max_memory_tb": 8.0,
"memory_types": ["DDR5 RDIMM"],
"max_dimm_speed_mts": 4800,
"pcie_slots_max": 3,
"drive_bays_max": 10,
"cpu_sockets": 2,
"best_practices": [
"16G DDR5: populate all memory channels evenly; follow white/black tab order in the ISM for the SKU.",
"Use matched DDR5 RDIMM kits; mixing ranks/speeds can downclock the entire domain.",
],
"performance_notes": [
"Sapphire Rapids / Emerald Rapids memory bandwidth benefits strongly from full channel population.",
],
"docs": [
{"title": "R660 Spec Sheet (PDF)", "url": _DELL_LINKS["spec_r660"], "kind": "spec"},
{"title": "R660 Technical Guide (PDF)", "url": _DELL_LINKS["tech_r660"], "kind": "tech"},
{"title": "Dell Support · R660 docs", "url": _DELL_LINKS["docs_r660"], "kind": "support"},
],
"slot_labels": [f"A{i}" for i in range(1, 17)] + [f"B{i}" for i in range(1, 17)],
},
"PowerEdge XR5610": {
"generation": "16G-XR",
"dimm_slots": 8,
"dimms_per_cpu": 8,
"channels_per_cpu": 8,
"max_memory_tb": 1.0,
"max_memory_note": "Dell Spec Sheet: 8× DDR5 RDIMM, up to 1 TB, up to 5600 MT/s (CPU/SKU dependent)",
"memory_types": ["DDR5 RDIMM"],
"max_dimm_speed_mts": 5600,
"pcie_slots_max": 2,
"pcie_note": "Spec Sheet: up to 2× PCIe Gen5 x16 (+ OCP 3.0) — riser/GPU option dependent",
"drive_bays_max": 4,
"drive_note": "Spec Sheet: up to 4× 2.5\" SAS/SATA/NVMe front; BOSS-N1 M.2 is separate boot — not front-bay count",
"cpu_sockets": 1,
"best_practices": [
"Populate DDR5 channels evenly with matched RDIMMs.",
"Confirm exact DIMM map in the XR5610 Technical Guide / ISM for this BOM.",
],
"performance_notes": [
"Single-socket edge platform: memory bandwidth scales with channels populated.",
],
"docs": [
{"title": "XR5610 Spec Sheet (PDF)", "url": _DELL_LINKS["spec_xr5610"], "kind": "spec"},
{"title": "XR5610 Technical Guide (PDF)", "url": _DELL_LINKS["tech_xr5610"], "kind": "tech"},
{"title": "Dell Support · XR5610 docs", "url": _DELL_LINKS["docs_xr5610"], "kind": "support"},
],
"slot_labels": [f"A{i}" for i in range(1, 9)],
},
}
def _normalize_model_key(model: str | None) -> str:
m = (model or "").strip()
if not m:
return ""
if m in MODEL_EXPANSION:
return m
# fuzzy family match
for key in MODEL_EXPANSION:
if key.lower() in m.lower() or m.lower() in key.lower():
return key
# strip suffixes like "vSAN Ready Node"
for key in MODEL_EXPANSION:
short = key.replace("PowerEdge ", "")
if short.lower() in m.lower():
return key
return m
def _parse_dimm_socket(name: str | None) -> tuple[str | None, int | None]:
import re
s = name or ""
m = re.search(r"(?:DIMM\.Socket\.|DIMM\s*)([A-D])(\d{1,2})", s, re.I)
if not m:
m = re.search(r"\b([A-D])(\d{1,2})\b", s)
if not m:
return None, None
return m.group(1).upper(), int(m.group(2))
def _disk_size_gb(raw: Any) -> float | None:
"""OME serverArrayDisks.Size is already capacity in GB (e.g. '446.62')."""
if raw is None or raw == "":
return None
try:
return round(float(str(raw).strip()), 2)
except Exception:
return None
def _mem_size_gb(raw: Any) -> float | None:
"""OME serverMemoryDevices.Size is capacity in MB."""
if raw is None or raw == "":
return None
try:
return round(float(raw) / 1024.0, 1)
except Exception:
return None
def build_expansion_report(model: str | None, inventory: dict) -> dict:
"""Split live OME facts (current) from Dell catalog max expansion (recommendations)."""
key = _normalize_model_key(model)
catalog_known = bool(MODEL_EXPANSION.get(key))
spec = dict(MODEL_EXPANSION.get(key) or {})
if not catalog_known:
spec = {
"generation": None,
"dimm_slots": None,
"max_memory_tb": None,
"pcie_slots_max": None,
"drive_bays_max": None,
"cpu_sockets": None,
"best_practices": [
"No Cockpit catalog entry for this model — verify DIMM/CPU/drive maxima in the Dell Tech Specs / ISM for the exact Service Tag BOM.",
],
"performance_notes": [],
"docs": [{"title": "Dell Support home", "url": _DELL_LINKS["support_home"]}],
"slot_labels": [],
"channel_map": {},
"memory_types": [],
"max_memory_note": "Maximum not asserted — catalog unknown for this model.",
"drive_note": "Bay maximum not asserted — catalog unknown for this model.",
"pcie_note": "PCIe maximum not asserted — catalog unknown for this model.",
}
mem_raw = inventory.get("serverMemoryDevices") or []
cpu_raw = inventory.get("serverProcessors") or []
disk_raw = inventory.get("serverArrayDisks") or []
cards_raw = inventory.get("serverDeviceCards") or []
raid_raw = inventory.get("serverRaidControllers") or []
os_raw = inventory.get("serverOperatingSystems") or []
dimm_modules: list[dict] = []
for dimm in mem_raw:
bank, num = _parse_dimm_socket(
dimm.get("Name") or dimm.get("InstanceId") or dimm.get("DeviceDescription")
)
if not bank or not num:
continue
label = f"{bank}{num}"
size_gb = _mem_size_gb(dimm.get("Size"))
dimm_modules.append(
{
"slot": label,
"cpu_bank": bank,
"slot_num": num,
"size_gb": size_gb,
"size_mb": dimm.get("Size"),
"rated_mts": dimm.get("Speed"),
"operating_mts": dimm.get("CurrentOperatingSpeed"),
"rank": dimm.get("Rank"),
"type": dimm.get("TypeDetails") or dimm.get("MemoryType") or "DDR",
"manufacturer": dimm.get("Manufacturer"),
"part_number": dimm.get("PartNumber"),
"serial": dimm.get("SerialNumber"),
"status": dimm.get("Status"),
}
)
dimm_modules.sort(key=lambda s: (s.get("cpu_bank") or "", s.get("slot_num") or 0))
mem_total_gb = round(sum(s.get("size_gb") or 0 for s in dimm_modules), 1)
rated_speeds = sorted({s["rated_mts"] for s in dimm_modules if s.get("rated_mts") is not None})
op_speeds = sorted({s["operating_mts"] for s in dimm_modules if s.get("operating_mts") is not None})
cpus: list[dict] = []
for p in cpu_raw:
cpus.append(
{
"slot": p.get("SlotNumber") or p.get("InstanceId"),
"model": p.get("ModelName") or p.get("BrandName"),
"cores": p.get("NumberOfEnabledCores") or p.get("NumberOfCores"),
"threads": p.get("NumberOfEnabledThreads") or p.get("NumberOfThreads"),
"current_mhz": p.get("CurrentSpeed"),
"max_mhz": p.get("MaxSpeed"),
"status": p.get("Status"),
}
)
cpu_cores_total = sum(int(c.get("cores") or 0) for c in cpus)
disks: list[dict] = []
for d in disk_raw:
disks.append(
{
"bay": d.get("SlotNumber"),
"model": d.get("ModelNumber"),
"vendor": d.get("VendorName"),
"media": d.get("MediaType"),
"size_gb": _disk_size_gb(d.get("Size")),
"bus": d.get("BusType"),
"status": d.get("StatusString") or d.get("Status"),
"serial": d.get("SerialNumber"),
"enclosure": d.get("EnclosureId"),
"form_factor": d.get("FormFactor"),
}
)
disks.sort(key=lambda x: (str(x.get("bus") or ""), x.get("bay") is None, x.get("bay") or 0))
disk_total_gb = round(sum(d.get("size_gb") or 0 for d in disks), 2)
adapters: list[dict] = []
for c in cards_raw:
adapters.append(
{
"slot": str(c.get("SlotNumber") or ""),
"description": c.get("Description"),
"manufacturer": c.get("Manufacturer"),
"slot_type": c.get("SlotType"),
"width": c.get("DatabusWidth"),
}
)
for r in raid_raw:
adapters.append(
{
"slot": str(r.get("PciSlot") or r.get("Fqdd") or "RAID"),
"description": r.get("Name") or r.get("DeviceDescription"),
"manufacturer": "Dell PERC",
"slot_type": "RAID",
"firmware": r.get("FirmwareVersion"),
"cache_mb": r.get("CacheSizeInMb"),
}
)
os_info = []
for o in os_raw:
os_info.append(
{
"os_name": (o.get("OsName") or "").strip() or None,
"os_version": (o.get("OsVersion") or "").strip() or None,
"hostname": (o.get("Hostname") or "").strip() or None,
}
)
current = {
"source": "OME DeviceService inventory (live)",
"memory": {
"installed_gb": mem_total_gb,
"dimm_count": len(dimm_modules),
"modules": dimm_modules,
"rated_mts": rated_speeds,
"operating_mts": op_speeds,
"banks_seen": sorted({m["cpu_bank"] for m in dimm_modules if m.get("cpu_bank")}),
},
"cpu": {
"sockets_populated": len(cpus),
"cores_total": cpu_cores_total,
"processors": cpus,
},
"disks": {
"count": len(disks),
"capacity_gb": disk_total_gb,
"items": disks,
},
"adapters": adapters,
"os": os_info,
}
populated_map = {m["slot"]: m for m in dimm_modules}
labels = list(spec.get("slot_labels") or [])
slot_map: list[dict] = []
for label in labels:
bank = label[0]
num = int(label[1:] or 0)
ch_map = (spec.get("channel_map") or {}).get(bank) or {}
if label in populated_map:
row = dict(populated_map[label])
row["populated"] = True
row["channel"] = ch_map.get(num)
row["rank_color"] = "white" if num <= 6 else "black"
row["speed_mts"] = row.get("operating_mts") or row.get("rated_mts")
slot_map.append(row)
else:
slot_map.append(
{
"slot": label,
"cpu_bank": bank,
"slot_num": num,
"channel": ch_map.get(num),
"rank_color": "white" if num <= 6 else "black",
"populated": False,
}
)
for label, row in populated_map.items():
if label not in labels:
extra = dict(row)
extra["populated"] = True
extra["speed_mts"] = extra.get("operating_mts") or extra.get("rated_mts")
slot_map.append(extra)
empty_n = sum(1 for s in slot_map if not s.get("populated")) if labels else None
dimm_slots_catalog = spec.get("dimm_slots")
max_tb = spec.get("max_memory_tb")
headroom_gb = None
if max_tb is not None:
try:
headroom_gb = round(float(max_tb) * 1024 - mem_total_gb, 1)
except Exception:
headroom_gb = None
drive_max = spec.get("drive_bays_max")
free_bays = (int(drive_max) - len(disks)) if catalog_known and isinstance(drive_max, int) else None
cpu_max = spec.get("cpu_sockets")
free_cpu = (int(cpu_max) - len(cpus)) if catalog_known and isinstance(cpu_max, int) else None
findings: list[dict] = []
if catalog_known and labels:
by_bank: dict[str, list] = {}
for s in slot_map:
by_bank.setdefault(s.get("cpu_bank") or "?", []).append(s)
for bank, bank_slots in by_bank.items():
ch_counts: dict[int, int] = {}
for s in bank_slots:
if s.get("populated") and s.get("channel") is not None:
ch_counts[int(s["channel"])] = ch_counts.get(int(s["channel"]), 0) + 1
if ch_counts and spec.get("channels_per_cpu"):
vals = list(ch_counts.values())
if max(vals) - min(vals) > 0 or len(ch_counts) < int(spec["channels_per_cpu"]):
findings.append(
{
"severity": "warning",
"code": "unbalanced_channels",
"text": f"Bank {bank}: channel population {dict(sorted(ch_counts.items()))} — Dell guidance prefers even channel fill for bandwidth.",
}
)
if len(op_speeds) > 1:
findings.append(
{
"severity": "warning",
"code": "mixed_operating_speeds",
"text": f"OME reports mixed operating speeds {op_speeds} MT/s — controller usually clocks all DIMMs to the lowest.",
}
)
if rated_speeds and op_speeds and max(op_speeds) < max(rated_speeds):
findings.append(
{
"severity": "info",
"code": "downclocked",
"text": f"DIMMs rated {rated_speeds} MT/s, operating at {op_speeds} MT/s (CPU/population limited).",
}
)
expansion = {
"source": "Dell model catalog in Cockpit (not live measurement)" if catalog_known else "Catalog unknown",
"catalog_known": catalog_known,
"catalog_model": key if catalog_known else None,
"generation": spec.get("generation"),
"memory": {
"max_capacity_tb": max_tb,
"max_capacity_gb": round(float(max_tb) * 1024, 1) if max_tb is not None else None,
"dimm_slots_max": dimm_slots_catalog,
"dimms_installed": len(dimm_modules),
"empty_slots_estimate": empty_n if catalog_known else None,
"headroom_gb": headroom_gb if catalog_known else None,
"supported_types": spec.get("memory_types") or [],
"max_rated_mts": spec.get("max_dimm_speed_mts"),
"note": spec.get("max_memory_note")
or (
f"Catalog max {max_tb} TB / {dimm_slots_catalog} DIMM slots"
if catalog_known
else "No catalog max — do not treat empty map as measured free slots."
),
"slot_map": slot_map if catalog_known else [],
},
"cpu": {
"sockets_max": cpu_max,
"sockets_populated": len(cpus),
"sockets_free_estimate": free_cpu,
"note": (
f"Chassis supports up to {cpu_max} CPU socket(s); OME sees {len(cpus)} populated."
if catalog_known and cpu_max is not None
else "CPU socket maximum not in Cockpit catalog for this model."
),
},
"disks": {
"bays_max": drive_max if catalog_known else None,
"drives_installed": len(disks),
"bays_free_estimate": free_bays,
"note": spec.get("drive_note")
or (
"Bay maximum from Dell chassis options — verify BOM; M.2/BOSS may not count as front bays."
if catalog_known
else "Drive bay maximum unknown for this model."
),
},
"pcie": {
"slots_max": spec.get("pcie_slots_max") if catalog_known else None,
"note": spec.get("pcie_note")
or ("PCIe max depends on riser kit." if catalog_known else "PCIe maximum unknown for this model."),
},
"best_practices": spec.get("best_practices") or [],
"performance_notes": spec.get("performance_notes") or [],
"docs": spec.get("docs") or [],
}
return {
"model": model,
"catalog_key": key if catalog_known else None,
"generic": not catalog_known,
"current": current,
"expansion": expansion,
"findings": findings,
"spec": {
"generation": expansion.get("generation"),
"dimm_slots": expansion["memory"].get("dimm_slots_max"),
"max_memory_tb": expansion["memory"].get("max_capacity_tb"),
"max_memory_note": expansion["memory"].get("note"),
"memory_types": expansion["memory"].get("supported_types"),
"max_dimm_speed_mts": expansion["memory"].get("max_rated_mts"),
"pcie_slots_max": expansion["pcie"].get("slots_max"),
"pcie_note": expansion["pcie"].get("note"),
"drive_bays_max": expansion["disks"].get("bays_max"),
"drive_note": expansion["disks"].get("note"),
"cpu_sockets": expansion["cpu"].get("sockets_max"),
"best_practices": expansion.get("best_practices") or [],
"performance_notes": expansion.get("performance_notes") or [],
"docs": expansion.get("docs") or [],
},
"memory": {
"total_gb": mem_total_gb,
"populated": len(dimm_modules),
"empty": empty_n,
"headroom_gb": headroom_gb,
"slots": slot_map if slot_map else [{**m, "populated": True, "speed_mts": m.get("operating_mts") or m.get("rated_mts")} for m in dimm_modules],
"banks": current["memory"]["banks_seen"],
},
"processors": cpus,
"disks": {
"installed": len(disks),
"max_bays": drive_max if catalog_known else None,
"free_bays_estimate": free_bays,
"items": [{**d, "slot": d.get("bay"), "name": d.get("model")} for d in disks],
},
"pcie": {
"max_slots": expansion["pcie"].get("slots_max"),
"items": adapters,
"note": expansion["pcie"].get("note"),
},
}
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
def pick_management(device: dict) -> dict:
"""Extract iDRAC/management IP plus OS hostname hints from OME DeviceManagement."""
mgmt = (device.get("DeviceManagement") or [{}])[0] or {}
idrac_ip = None
for m in device.get("DeviceManagement") or []:
addr = m.get("NetworkAddress")
if addr and str(addr).count(".") == 3:
idrac_ip = addr
mgmt = m
break
os_hostname = (mgmt.get("InstrumentationName") or "").strip() or None
dns_name = (mgmt.get("DnsName") or "").strip() or None
return {
"idrac_ip": idrac_ip,
"os_hostname": os_hostname,
"mgmt_dns_name": dns_name,
"mac": mgmt.get("MacAddress"),
}
async def resolve_host_ips(hostname: str | None) -> list[str]:
if not hostname or hostname.count(".") == 3:
return []
loop = asyncio.get_running_loop()
try:
infos = await loop.getaddrinfo(hostname, None, family=socket.AF_INET)
return sorted({i[4][0] for i in infos})
except Exception:
return []
async def enrich_rdp_targets(devices: list[dict]) -> None:
"""DNS-resolve OS hostnames from OME InstrumentationName → RDP candidate IPs."""
hostnames: set[str] = set()
for n in devices:
h = (n.get("os_hostname") or "").strip()
if h and h.count(".") != 3:
hostnames.add(h)
# also try short name
if "." in h:
hostnames.add(h.split(".", 1)[0])
resolved: dict[str, list[str]] = {}
sem = asyncio.Semaphore(20)
async def one(h: str):
async with sem:
resolved[h] = await resolve_host_ips(h)
if hostnames:
await asyncio.gather(*[one(h) for h in hostnames])
for n in devices:
idrac = n.get("idrac_ip") or n.get("ip")
h = (n.get("os_hostname") or "").strip()
ips: list[str] = []
seen: set[str] = set()
for candidate in (h, h.split(".", 1)[0] if h and "." in h else None):
if not candidate:
continue
for ip in resolved.get(candidate) or []:
if ip not in seen:
seen.add(ip)
ips.append(ip)
# Prefer OS IPs that differ from iDRAC
os_ips = [ip for ip in ips if ip != idrac]
rdp_ips = os_ips or ips
n["rdp_ips"] = rdp_ips
n["rdp_host"] = rdp_ips[0] if rdp_ips else None
n["os_ips"] = os_ips
# Windows hint from name / later OS inventory
blob = f"{n.get('name') or ''} {n.get('os_hostname') or ''} {n.get('os_name') or ''}".lower()
n["is_windows"] = any(k in blob for k in ("windows", "hyperv", "hyper-v", "-hv", "hv0")) or bool(
n.get("os_name") and "windows" in str(n.get("os_name")).lower()
)
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 = []
model_counts: dict[str, int] = defaultdict(int)
idrac_count = server_count = connected = powered = 0
switch_count = chassis_count = pdu_count = storage_count = 0
total_watts = 0.0
watts_samples = 0
status_counts: dict[str, int] = defaultdict(int)
for d in devices_raw:
mgmt = pick_management(d)
ip = mgmt.get("idrac_ip") or pick_ip(d)
subnet = subnet_of(ip)
dtype = d.get("Type")
sub = d.get("SubDeviceType") or ""
model = d.get("Model") or "Unknown"
is_server = dtype == 1000
is_idrac = sub == "iDRAC"
is_switch = dtype == 7000 or "switch" in (sub or "").lower() or "switch" in model.lower()
is_chassis = dtype == 2000 or "chassis" in (sub or "").lower()
is_pdu = dtype == 100
is_storage = dtype == 5000 or "storage" in (sub or "").lower()
if is_switch:
role = "switch"
elif is_chassis:
role = "chassis"
elif is_pdu:
role = "pdu"
elif is_storage:
role = "storage"
elif is_server:
role = "server"
else:
role = "other"
conn = bool(d.get("ConnectionState"))
power_state = d.get("PowerState")
status = str(d.get("Status") or "")
status_counts[status] += 1
if is_server:
model_counts[model] += 1
if is_idrac:
idrac_count += 1
if is_switch:
switch_count += 1
if is_chassis:
chassis_count += 1
if is_pdu:
pdu_count += 1
if is_storage:
storage_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,
"idrac_ip": mgmt.get("idrac_ip") or ip,
"os_hostname": mgmt.get("os_hostname"),
"mgmt_dns_name": mgmt.get("mgmt_dns_name"),
"rdp_host": None,
"rdp_ips": [],
"os_ips": [],
"is_windows": False,
"subnet": subnet,
"role": role,
"is_server": is_server,
"is_idrac": is_idrac,
"is_switch": is_switch,
"is_chassis": is_chassis,
"is_pdu": is_pdu,
"is_storage": is_storage,
"chassis_service_tag": d.get("ChassisServiceTag"),
"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"),
}
node["map_cidrs"] = [subnet] if subnet and subnet != "unknown" else []
node["extra_ips"] = []
devices.append(node)
await enrich_rdp_targets(devices)
sync_endpoints_from_fleet(devices)
apply_network_endpoints(devices)
subnets = build_subnet_summaries(devices)
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_switch"):
return "switch"
if n.get("is_chassis"):
return "chassis"
if n.get("is_idrac"):
return "iDRAC"
if n.get("is_server"):
return "server"
if n.get("is_storage"):
return "storage"
if n.get("is_pdu"):
return "PDU"
return n.get("role") or "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,
"switches": switch_count,
"chassis": chassis_count,
"pdus": pdu_count,
"storage": storage_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)
def _seed_atc_racks() -> None:
"""Idempotent seed: ATC1 (8×42U) + ATC2 (6×42U)."""
now = time.time()
with _db() as conn:
n = conn.execute("SELECT COUNT(*) AS c FROM racks").fetchone()["c"]
if n > 0:
return
conn.execute(
"INSERT OR IGNORE INTO sites(id, name, sort_order) VALUES (?, ?, ?)",
("ATC1", "ATC1", 1),
)
conn.execute(
"INSERT OR IGNORE INTO sites(id, name, sort_order) VALUES (?, ?, ?)",
("ATC2", "ATC2", 2),
)
for i in range(1, 9):
conn.execute(
"""
INSERT INTO racks(site_id, name, units, sort_order, created_at, updated_at)
VALUES (?, ?, 42, ?, ?, ?)
""",
("ATC1", f"ATC1-R{i:02d}", i, now, now),
)
for i in range(1, 7):
conn.execute(
"""
INSERT INTO racks(site_id, name, units, sort_order, created_at, updated_at)
VALUES (?, ?, 42, ?, ?, ?)
""",
("ATC2", f"ATC2-R{i:02d}", i, now, now),
)
log.info("Seeded ATC racks: ATC1×8 + ATC2×6 (42U)")
def build_network_fabric(devices: list[dict] | None = None) -> dict:
"""Inferred switch↔server / chassis↔blade edges (no LLDP)."""
devices = list(devices if devices is not None else (STATE.get("devices") or []))
by_id = {d.get("id"): d for d in devices if d.get("id") is not None}
edges: list[dict] = []
seen: set[tuple] = set()
def add_edge(a: int, b: int, kind: str):
if a is None or b is None or a == b:
return
key = (min(a, b), max(a, b), kind)
if key in seen:
return
seen.add(key)
da, db = by_id.get(a) or {}, by_id.get(b) or {}
edges.append(
{
"from_id": a,
"to_id": b,
"kind": kind,
"confidence": "inferred",
"note": (
"management subnet"
if kind == "subnet"
else "MX chassis / blade"
),
"live": bool(da.get("connected") and db.get("connected")),
}
)
switches = [d for d in devices if d.get("is_switch")]
for sw in switches:
cidr = sw.get("subnet") or "unknown"
if cidr == "unknown":
continue
for d in devices:
if d.get("id") == sw.get("id"):
continue
if d.get("subnet") != cidr:
continue
if d.get("is_switch") or d.get("is_pdu"):
continue
# Prefer linking servers/storage/chassis endpoints
if d.get("is_server") or d.get("is_storage") or d.get("is_chassis") or d.get("is_idrac"):
add_edge(sw["id"], d["id"], "subnet")
chassis_list = [d for d in devices if d.get("is_chassis")]
for ch in chassis_list:
cst = (ch.get("service_tag") or "").upper()
for d in devices:
if not d.get("is_server"):
continue
model = (d.get("model") or "").upper()
blade_cst = (d.get("chassis_service_tag") or "").upper()
if cst and blade_cst and blade_cst == cst:
add_edge(ch["id"], d["id"], "chassis")
elif "MX" in model and "MX7000" not in model:
# fallback: all MX blades to MX chassis in same environment
add_edge(ch["id"], d["id"], "chassis")
return {
"edges": edges,
"note": "Edges inferred from management subnet / MX chassis — not LLDP.",
"switches": [
{
"id": s.get("id"),
"name": s.get("name"),
"service_tag": s.get("service_tag"),
"ip": s.get("ip") or s.get("idrac_ip"),
"subnet": s.get("subnet"),
"connected": s.get("connected"),
"model": s.get("model"),
"connected_device_ids": [
e["to_id"] if e["from_id"] == s.get("id") else e["from_id"]
for e in edges
if e["kind"] == "subnet"
and (e["from_id"] == s.get("id") or e["to_id"] == s.get("id"))
],
}
for s in switches
],
"summary": {
"devices": len(devices),
"edges": len(edges),
"switches": len(switches),
"chassis": sum(1 for d in devices if d.get("is_chassis")),
},
}
def _default_u_height(device: dict | None) -> int:
if not device:
return 1
cat = match_rack_catalog(device.get("model"), device.get("role"))
if cat is not None:
uh = int(cat.get("u_height") or 0)
return uh if uh > 0 else 1 # 0U vertical PDUs place as 1U marker unless side=left/right
if device.get("is_chassis"):
return 7
if device.get("is_storage"):
return 2
if device.get("is_switch"):
return 1
return 1
def _rack_overlap(
conn: sqlite3.Connection,
rack_id: int,
u_start: int,
u_height: int,
side: str = "front",
exclude_item_id: int | None = None,
exclude_device_id: int | None = None,
) -> bool:
if side in ("left", "right"):
return False # side PDUs do not consume front/rear U map
u_end = u_start + max(u_height, 1) - 1
rows = conn.execute(
"SELECT id, device_id, u_start, u_height, side FROM rack_items WHERE rack_id=?",
(rack_id,),
).fetchall()
for r in rows:
if exclude_item_id is not None and r["id"] == exclude_item_id:
continue
if exclude_device_id is not None and r["device_id"] == exclude_device_id:
continue
rside = (r["side"] or "front")
if rside in ("left", "right"):
continue
if rside != side:
continue
a, b = r["u_start"], r["u_start"] + r["u_height"] - 1
if not (u_end < a or u_start > b):
return True
return False
def _enrich_rack_item(item: dict, devices: dict) -> dict:
out = dict(item)
dev = devices.get(item.get("device_id")) if item.get("device_id") is not None else None
sku = item.get("catalog_sku")
cat = None
if sku:
cat = next((c for c in DELL_RACK_CATALOG if c["sku"] == sku), None)
if not cat and dev:
cat = match_rack_catalog(dev.get("model"), dev.get("role"))
if cat:
out["catalog"] = {k: cat[k] for k in ("sku", "name", "family", "category", "u_height", "face", "image") if k in cat}
out["face"] = cat.get("face")
out["image"] = cat.get("image") or catalog_image_for(cat)
out["category"] = cat.get("category")
else:
out["catalog"] = None
out["face"] = "custom"
out["image"] = FACE_IMAGE_MAP["custom"]
out["category"] = (dev or {}).get("role") or "custom"
if dev:
out["device"] = {
"id": dev.get("id"),
"name": dev.get("name"),
"model": dev.get("model"),
"service_tag": dev.get("service_tag"),
"role": dev.get("role"),
"ip": dev.get("ip") or dev.get("idrac_ip"),
"connected": dev.get("connected"),
"is_switch": dev.get("is_switch"),
"is_server": dev.get("is_server"),
"is_storage": dev.get("is_storage"),
"is_chassis": dev.get("is_chassis"),
"is_pdu": dev.get("is_pdu"),
}
out["label"] = item.get("label") or dev.get("name")
out["model"] = dev.get("model")
else:
out["device"] = None
out["label"] = item.get("label") or ((cat or {}).get("name") if cat else "Custom")
out["model"] = (cat or {}).get("name")
return out
def _racks_payload() -> dict:
_migrate_rack_items()
devices = {d.get("id"): d for d in (STATE.get("devices") or []) if d.get("id") is not None}
with _db() as conn:
sites = [dict(r) for r in conn.execute("SELECT * FROM sites ORDER BY sort_order").fetchall()]
racks = [dict(r) for r in conn.execute("SELECT * FROM racks ORDER BY site_id, sort_order").fetchall()]
items = [dict(r) for r in conn.execute("SELECT * FROM rack_items ORDER BY u_start DESC").fetchall()]
placed_ids = {i["device_id"] for i in items if i.get("device_id") is not None}
by_site: dict[str, list] = {}
for rk in racks:
card = dict(rk)
card["items"] = []
card["placements"] = [] # backward compat alias
for it in items:
if it["rack_id"] != rk["id"]:
continue
enriched = _enrich_rack_item(it, devices)
card["items"].append(enriched)
# compat shape for older UI bits
card["placements"].append(
{
"device_id": it.get("device_id"),
"rack_id": it["rack_id"],
"u_start": it["u_start"],
"u_height": it["u_height"],
"item_id": it["id"],
"side": it.get("side") or "front",
"face": enriched.get("face"),
"catalog": enriched.get("catalog"),
"label": enriched.get("label"),
"device": enriched.get("device") or {
"id": it.get("device_id"),
"name": enriched.get("label"),
"model": enriched.get("model"),
"role": enriched.get("category"),
"service_tag": None,
},
}
)
by_site.setdefault(rk["site_id"], []).append(card)
site_out = [{**s, "racks": by_site.get(s["id"], [])} for s in sites]
unplaced = []
for d in STATE.get("devices") or []:
if d.get("id") in placed_ids:
continue
if not (d.get("is_server") or d.get("is_switch") or d.get("is_chassis") or d.get("is_storage") or d.get("is_pdu")):
continue
cat = match_rack_catalog(d.get("model"), d.get("role"))
unplaced.append(
{
"id": d.get("id"),
"name": d.get("name"),
"model": d.get("model"),
"service_tag": d.get("service_tag"),
"role": d.get("role"),
"ip": d.get("ip") or d.get("idrac_ip"),
"connected": d.get("connected"),
"default_u_height": _default_u_height(d),
"catalog_sku": (cat or {}).get("sku"),
"face": (cat or {}).get("face") or "custom",
"image": (cat or {}).get("image") or catalog_image_for(cat or {"face": "custom"}),
"is_switch": d.get("is_switch"),
"is_server": d.get("is_server"),
"is_storage": d.get("is_storage"),
"is_chassis": d.get("is_chassis"),
"is_pdu": d.get("is_pdu"),
}
)
unplaced.sort(key=lambda x: ((x.get("role") or ""), (x.get("name") or "").lower()))
return {
"sites": site_out,
"items": items,
"placements": items,
"unplaced": unplaced,
"catalog": DELL_RACK_CATALOG,
}
def _switch_port_count(device: dict | None) -> int:
"""Default front-panel port count from model (OME has no LLDP ports here)."""
model = ((device or {}).get("model") or "").lower()
if "s4048" in model:
return 48
if "s5248" in model or "s5296" in model:
return 48
if "s4148" in model:
return 48
if "mx9116" in model:
return 16
if "mx5108" in model:
return 8
return 48
def _port_label(port_num: int, total: int = 48) -> str:
return f"Eth1/{port_num}"
def build_switch_portmap(switch_id: int) -> dict:
devices = {d.get("id"): d for d in (STATE.get("devices") or []) if d.get("id") is not None}
sw = devices.get(switch_id)
if not sw or not sw.get("is_switch"):
# still allow if type 7000 missing flag somehow
sw = next((d for d in (STATE.get("devices") or []) if d.get("id") == switch_id), None)
if not sw:
raise HTTPException(404, "Switch not found in fleet")
nports = _switch_port_count(sw)
with _db() as conn:
rows = conn.execute(
"SELECT * FROM port_links WHERE switch_id=? ORDER BY switch_port",
(switch_id,),
).fetchall()
links = [dict(r) for r in rows]
by_port = {int(l["switch_port"]): l for l in links}
ports = []
for i in range(1, nports + 1):
link = by_port.get(i)
peer = devices.get(link["device_id"]) if link else None
ports.append(
{
"port": i,
"label": _port_label(i, nports),
"wired": bool(link),
"link": (
{
"id": link["id"],
"device_id": link["device_id"],
"device_port": link.get("device_port") or "",
"note": link.get("note") or "",
"device": {
"id": peer.get("id"),
"name": peer.get("name"),
"service_tag": peer.get("service_tag"),
"ip": peer.get("ip") or peer.get("idrac_ip"),
"model": peer.get("model"),
"role": peer.get("role"),
"connected": peer.get("connected"),
}
if peer
else None,
}
if link
else None
),
}
)
return {
"switch": {
"id": sw.get("id"),
"name": sw.get("name"),
"model": sw.get("model"),
"service_tag": sw.get("service_tag"),
"ip": sw.get("ip") or sw.get("idrac_ip"),
"subnet": sw.get("subnet"),
"connected": sw.get("connected"),
"port_count": nports,
},
"ports": ports,
"wired_count": sum(1 for p in ports if p["wired"]),
"note": "Port wiring is manual (OME has no LLDP port map for this switch). Map switch port → server NIC/port.",
}
def _seed_atc_vlans() -> None:
"""Seed known ATC lab VLANs/subnets (editable). Ensures new catalog entries are added."""
now = time.time()
defaults = [
(40, "iDRAC / OOB A", "10.0.40.0/24", "Out-of-band management (has S4048)", "ATC1", "#00a8e8", 1),
(41, "iDRAC / OOB B", "10.0.41.0/24", "Out-of-band management", "ATC1", "#3dffe0", 2),
(42, "iDRAC / OOB C", "10.0.42.0/24", "Out-of-band management", "ATC2", "#7c5cff", 3),
(10, "Production / OS", "10.0.10.0/24", "Hypervisor & Windows OS / RDP", "ATC1", "#ff9a3c", 4),
(11, "Lab / OS", "10.0.11.0/24", "Lab OS network", "ATC2", "#ff5c5c", 5),
(20, "Shared services", "10.0.20.0/24", "Mgmt / demo / shared ATC services", "ATC1", "#a3e635", 6),
(120, "Secondary OS", "10.0.120.0/24", "Secondary OS / RDP path", "ATC1", "#f0d060", 7),
(90, "FDE cluster / AI", "10.0.90.0/24", "FDE platform · DNS · HAProxy · OPNsense · Proxmox (dell-fde.lan)", "FDE", "#38bdf8", 8),
]
with _db() as conn:
existing = {
int(r["vlan_id"])
for r in conn.execute("SELECT vlan_id FROM vlans WHERE vlan_id IS NOT NULL").fetchall()
if r["vlan_id"] is not None
}
added = 0
for vlan_id, name, cidr, purpose, site, color, so in defaults:
if vlan_id in existing:
continue
conn.execute(
"""
INSERT INTO vlans(vlan_id, name, cidr, purpose, site_id, color, sort_order, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(vlan_id, name, cidr, purpose, site, color, so, now, now),
)
added += 1
if added:
log.info("Seeded ATC VLAN catalog (+%s new, %s known defaults)", added, len(defaults))
def _synthetic_device_id(ip: str) -> int:
"""Stable negative id for inventory-only hosts (safe for JS Number)."""
try:
a, b, c, d = (int(x) for x in ip.split("."))
return -(a * 1_000_000 + b * 10_000 + c * 100 + d)
except Exception:
return -abs(hash(ip)) % 1_000_000_000
def _vlan_id_for_ip(ip: str) -> int | None:
try:
return int(ip.split(".")[2])
except Exception:
return None
def _ensure_network_endpoints_schema(conn) -> None:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS network_endpoints (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hostname TEXT,
ip TEXT NOT NULL UNIQUE,
role TEXT NOT NULL DEFAULT 'server',
kind TEXT NOT NULL DEFAULT 'physical',
device_id INTEGER,
vlan_id INTEGER,
switch_id INTEGER,
switch_port INTEGER,
model TEXT,
note TEXT,
user_note TEXT,
created_at REAL NOT NULL,
updated_at REAL NOT NULL
)
"""
)
cols = {r[1] for r in conn.execute("PRAGMA table_info(network_endpoints)").fetchall()}
if "user_note" not in cols:
conn.execute("ALTER TABLE network_endpoints ADD COLUMN user_note TEXT")
def _upsert_network_endpoint(
*,
hostname: str | None,
ip: str,
role: str = "server",
kind: str = "host",
device_id: int | None = None,
vlan_id: int | None = None,
switch_id: int | None = None,
switch_port: int | None = None,
model: str | None = None,
note: str | None = None,
overwrite_identity: bool = False,
clear_device_id: bool = False,
) -> None:
"""Insert/update inventory row. Never clobber user_note. Soft-update identity fields."""
ip = (ip or "").strip()
if not ip or ip.count(".") != 3:
return
now = time.time()
vlan_id = vlan_id if vlan_id is not None else _vlan_id_for_ip(ip)
with _db() as conn:
_ensure_network_endpoints_schema(conn)
row = conn.execute("SELECT * FROM network_endpoints WHERE ip=?", (ip,)).fetchone()
if not row:
conn.execute(
"""
INSERT INTO network_endpoints(
hostname, ip, role, kind, device_id, vlan_id, switch_id, switch_port,
model, note, user_note, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
""",
(
hostname,
ip,
role or "server",
kind or "host",
device_id,
vlan_id,
switch_id,
switch_port,
model,
note,
now,
now,
),
)
return
# Update carefully
sets = ["updated_at=?"]
vals: list = [now]
if overwrite_identity or not row["hostname"]:
if hostname:
sets.append("hostname=?")
vals.append(hostname)
if overwrite_identity or not row["model"]:
if model:
sets.append("model=?")
vals.append(model)
if overwrite_identity or not row["note"]:
if note:
sets.append("note=?")
vals.append(note)
if overwrite_identity and note is not None:
sets.append("note=?")
vals.append(note)
if overwrite_identity and model is not None:
if "model=?" not in sets:
sets.append("model=?")
vals.append(model)
if overwrite_identity and hostname is not None:
if "hostname=?" not in sets:
sets.append("hostname=?")
vals.append(hostname)
if role:
sets.append("role=?")
vals.append(role)
if kind:
sets.append("kind=?")
vals.append(kind)
if vlan_id is not None:
sets.append("vlan_id=?")
vals.append(vlan_id)
if clear_device_id:
sets.append("device_id=NULL")
elif device_id is not None and (overwrite_identity or row["device_id"] is None):
sets.append("device_id=?")
vals.append(device_id)
if switch_id is not None and (overwrite_identity or row["switch_id"] is None):
sets.append("switch_id=?")
vals.append(switch_id)
if switch_port is not None and (overwrite_identity or row["switch_port"] is None):
sets.append("switch_port=?")
vals.append(switch_port)
vals.append(ip)
conn.execute(f"UPDATE network_endpoints SET {', '.join(sets)} WHERE ip=?", vals)
def _known_host_inventory() -> list[dict]:
"""Static + discovered lab hosts (OS / FDE / shared services)."""
return [
# VLAN 90 — FDE (physical Proxmox nodes are R640 per ops; OME idrac-proxmox R740xd is a different box)
{"hostname": "router.dell-fde.lan", "ip": "10.0.90.1", "role": "gateway", "kind": "appliance", "vlan_id": 90, "model": None, "note": "FDE gateway"},
{"hostname": "ns.dell-fde.lan", "ip": "10.0.90.2", "role": "dns", "kind": "vm", "vlan_id": 90, "note": "FDE DNS"},
{"hostname": "haproxy.dell-fde.lan", "ip": "10.0.90.3", "role": "lb", "kind": "vm", "vlan_id": 90, "note": "FDE HAProxy"},
{"hostname": "caddy.dell-fde.lan", "ip": "10.0.90.4", "role": "proxy", "kind": "vm", "vlan_id": 90, "note": "FDE Caddy"},
{"hostname": "opnsense01.dell-fde.lan", "ip": "10.0.90.5", "role": "firewall", "kind": "vm", "vlan_id": 90, "note": "FDE OPNsense 1"},
{"hostname": "opnsense02.dell-fde.lan", "ip": "10.0.90.6", "role": "firewall", "kind": "vm", "vlan_id": 90, "note": "FDE OPNsense 2"},
{
"hostname": "proxmox01.dell-fde.lan",
"ip": "10.0.90.21",
"role": "server",
"kind": "physical",
"vlan_id": 90,
"model": "PowerEdge R640",
"device_id": None,
"switch_id": 51950,
"note": "FDE Proxmox node 1 · R640 (ops). Candidate iDRAC: C90XK53/B90XK53 on VLAN41 — not the R740xd idrac-proxmox.",
"overwrite_identity": True,
"clear_device_id": True,
},
{
"hostname": "proxmox02.dell-fde.lan",
"ip": "10.0.90.22",
"role": "server",
"kind": "physical",
"vlan_id": 90,
"model": "PowerEdge R640",
"device_id": None,
"switch_id": 51950,
"note": "FDE Proxmox node 2 · R640 (ops). Candidate iDRAC: C90XK53/B90XK53 on VLAN41.",
"overwrite_identity": True,
"clear_device_id": True,
},
{"hostname": "fde-dns.fde-dns.lan", "ip": "10.0.90.41", "role": "dns", "kind": "vm", "vlan_id": 90},
{"hostname": "dns-server.fde-dns.lan", "ip": "10.0.90.42", "role": "dns", "kind": "vm", "vlan_id": 90},
{"hostname": "db01.dell-fde.lan", "ip": "10.0.90.181", "role": "database", "kind": "vm", "vlan_id": 90},
# VLAN 10 — Production OS
{"hostname": "cop-hv01.dell-atc.lan", "ip": "10.0.10.13", "role": "hypervisor", "kind": "physical", "vlan_id": 10},
{"hostname": "cop-dc01.dell-atc.lan", "ip": "10.0.10.15", "role": "dc", "kind": "vm", "vlan_id": 10},
{"hostname": "cop-dc02.dell-atc.lan", "ip": "10.0.10.16", "role": "dc", "kind": "vm", "vlan_id": 10},
{"hostname": "cop-vsan-vdi01.dell-atc.lan", "ip": "10.0.10.41", "role": "hypervisor", "kind": "physical", "vlan_id": 10},
{"hostname": "cop-vsan-vdi02.dell-atc.lan", "ip": "10.0.10.42", "role": "hypervisor", "kind": "physical", "vlan_id": 10},
{"hostname": "cop-vsan-vdi03.dell-atc.lan", "ip": "10.0.10.43", "role": "hypervisor", "kind": "physical", "vlan_id": 10},
{"hostname": "cop-vsan-vdi04.dell-atc.lan", "ip": "10.0.10.44", "role": "hypervisor", "kind": "physical", "vlan_id": 10},
{"hostname": "pve01.dell-atc.dell.nl", "ip": "10.0.10.65", "role": "server", "kind": "physical", "vlan_id": 10, "note": "Proxmox VE (ATC)"},
{"hostname": "atc-gpu-prod.dell-atc.lan", "ip": "10.0.10.106", "role": "gpu", "kind": "physical", "vlan_id": 10},
# VLAN 20 — Shared services
{"hostname": "cov-dc03.dell-atc.lan", "ip": "10.0.20.15", "role": "dc", "kind": "vm", "vlan_id": 20},
{"hostname": "cov-omedemo01.dell-atc.lan", "ip": "10.0.20.22", "role": "ome", "kind": "vm", "vlan_id": 20},
{"hostname": "cov-cloudiq01.dell-atc.lan", "ip": "10.0.20.23", "role": "cloudiq", "kind": "vm", "vlan_id": 20},
{"hostname": "cov-scg01.dell-atc.lan", "ip": "10.0.20.31", "role": "scg", "kind": "vm", "vlan_id": 20},
{"hostname": "cov-vcsa-vsan01.dell-atc.lan", "ip": "10.0.20.40", "role": "vcenter", "kind": "vm", "vlan_id": 20},
{"hostname": "photon-machine.dell-atc.lan", "ip": "10.0.20.102", "role": "host", "kind": "vm", "vlan_id": 20},
{"hostname": "atc-grafana.dell-atc.lan", "ip": "10.0.20.103", "role": "monitoring", "kind": "vm", "vlan_id": 20},
{"hostname": "atc-mgt01.dell-atc.lan", "ip": "10.0.20.104", "role": "mgmt", "kind": "vm", "vlan_id": 20},
{"hostname": "atc-dataflow01.dell-atc.lan", "ip": "10.0.20.105", "role": "dataflow", "kind": "vm", "vlan_id": 20},
{"hostname": "atc-gpu-dev.dell-atc.lan", "ip": "10.0.20.106", "role": "gpu", "kind": "vm", "vlan_id": 20},
{"hostname": "atc-spearmint.dell-atc.lan", "ip": "10.0.20.108", "role": "app", "kind": "vm", "vlan_id": 20},
{"hostname": "cov-file01.dell-atc.lan", "ip": "10.0.20.109", "role": "files", "kind": "vm", "vlan_id": 20},
{"hostname": "atc-nas.dell-atc.lan", "ip": "10.0.20.110", "role": "nas", "kind": "appliance", "vlan_id": 20},
{"hostname": "atc-objectscale.dell-atc.lan", "ip": "10.0.20.111", "role": "object", "kind": "appliance", "vlan_id": 20},
{"hostname": "atc-db01.dell-atc.lan", "ip": "10.0.20.112", "role": "database", "kind": "vm", "vlan_id": 20},
{"hostname": "atc-backup.dell-atc.lan", "ip": "10.0.20.113", "role": "backup", "kind": "vm", "vlan_id": 20},
{"hostname": "opnsense03.dell-atc.lan", "ip": "10.0.20.114", "role": "firewall", "kind": "vm", "vlan_id": 20},
{"hostname": "atc-git.dell-atc.lan", "ip": "10.0.20.118", "role": "git", "kind": "vm", "vlan_id": 20},
{"hostname": "cvd-jody01.dell-atc.lan", "ip": "10.0.20.119", "role": "desktop", "kind": "vm", "vlan_id": 20},
{"hostname": "emj-wilcor01.dell-atc.lan", "ip": "10.0.20.133", "role": "desktop", "kind": "vm", "vlan_id": 20},
{"hostname": "cov-wac01.dell-atc.lan", "ip": "10.0.20.147", "role": "wac", "kind": "vm", "vlan_id": 20},
{"hostname": "CVP-MGMT01.dell-atc.lan", "ip": "10.0.20.164", "role": "mgmt", "kind": "vm", "vlan_id": 20},
]
def _seed_network_endpoints() -> None:
"""Ensure schema + known host inventory + correct FDE Proxmox identity."""
with _db() as conn:
_ensure_network_endpoints_schema(conn)
for h in _known_host_inventory():
_upsert_network_endpoint(
hostname=h.get("hostname"),
ip=h["ip"],
role=h.get("role") or "server",
kind=h.get("kind") or "host",
device_id=h.get("device_id"),
vlan_id=h.get("vlan_id"),
switch_id=h.get("switch_id"),
switch_port=h.get("switch_port"),
model=h.get("model"),
note=h.get("note"),
overwrite_identity=bool(h.get("overwrite_identity")),
clear_device_id=bool(h.get("clear_device_id")),
)
def sync_endpoints_from_fleet(devices: list[dict] | None = None) -> int:
"""Upsert every OME fleet mgmt IP into network_endpoints (enables notes on all systems)."""
_seed_network_endpoints()
devices = devices if devices is not None else (STATE.get("devices") or [])
n = 0
for d in devices:
if d.get("source") == "inventory":
continue
did = d.get("id")
if not isinstance(did, int) or did <= 0:
continue
ip = d.get("idrac_ip") or d.get("ip")
if not ip:
continue
role = d.get("role") or "server"
kind = "switch" if d.get("is_switch") else ("pdu" if d.get("is_pdu") else ("chassis" if d.get("is_chassis") else "physical"))
_upsert_network_endpoint(
hostname=d.get("name"),
ip=ip,
role=role,
kind=kind,
device_id=did,
vlan_id=_vlan_id_for_ip(ip),
model=d.get("model"),
note=f"OME {d.get('service_tag') or did}",
)
n += 1
# Also register secondary OS IPs from rdp/os/extra
for sip in (d.get("rdp_ips") or []) + (d.get("os_ips") or []) + (d.get("extra_ips") or []):
if not sip or sip == ip:
continue
_upsert_network_endpoint(
hostname=d.get("cluster_hostname") or d.get("os_hostname") or d.get("name"),
ip=sip,
role=role,
kind="os",
device_id=did,
vlan_id=_vlan_id_for_ip(sip),
model=d.get("model"),
note=f"OS/secondary IP for OME {d.get('service_tag') or did}",
)
n += 1
return n
def _load_network_endpoints() -> list[dict]:
_seed_network_endpoints()
with _db() as conn:
_ensure_network_endpoints_schema(conn)
return [dict(r) for r in conn.execute("SELECT * FROM network_endpoints ORDER BY ip").fetchall()]
def _load_vlan_catalog_by_cidr() -> dict[str, dict]:
_seed_atc_vlans()
with _db() as conn:
rows = [dict(r) for r in conn.execute("SELECT * FROM vlans").fetchall()]
out = {}
for r in rows:
cidr = (r.get("cidr") or "").strip()
if cidr:
out[cidr] = r
return out
def _device_ips(d: dict) -> list[str]:
out: list[str] = []
for k in ("ip", "idrac_ip"):
v = d.get(k)
if v and v not in out:
out.append(v)
for key in ("rdp_ips", "os_ips", "extra_ips"):
for v in d.get(key) or []:
if v and v not in out:
out.append(v)
return out
def _refresh_map_cidrs(d: dict) -> None:
cidrs: list[str] = []
seen: set[str] = set()
primary = d.get("subnet")
if primary and primary != "unknown" and primary not in seen:
seen.add(primary)
cidrs.append(primary)
for ip in _device_ips(d):
c = subnet_of(ip)
if c != "unknown" and c not in seen:
seen.add(c)
cidrs.append(c)
d["map_cidrs"] = cidrs
def apply_network_endpoints(devices: list[dict]) -> None:
"""Attach inventory IPs to OME devices and inject synthetic hosts for unlinked endpoints."""
endpoints = _load_network_endpoints()
if not endpoints:
for d in devices:
_refresh_map_cidrs(d)
return
by_id = {d.get("id"): d for d in devices if d.get("id") is not None}
# Index existing IPs on devices to avoid duplicate synthetics for OME mgmt IPs
device_ips: set[str] = set()
for d in devices:
for ip in _device_ips(d):
device_ips.add(ip)
existing_ids = set(by_id)
used_synth: set[int] = set()
for ep in endpoints:
ip = (ep.get("ip") or "").strip()
if not ip or ip.count(".") != 3:
continue
hostname = (ep.get("hostname") or ip).strip()
did = ep.get("device_id")
user_note = ep.get("user_note") or ""
sys_note = ep.get("note") or ""
if did is not None and did in by_id:
d = by_id[did]
extras = list(d.get("extra_ips") or [])
# Only treat as extra if not already primary mgmt IP
if ip not in extras and ip != d.get("ip") and ip != d.get("idrac_ip"):
extras.append(ip)
d["extra_ips"] = extras
if ip != d.get("ip") and ip != d.get("idrac_ip"):
os_ips = list(d.get("os_ips") or [])
if ip not in os_ips:
os_ips.append(ip)
d["os_ips"] = os_ips
rdp = list(d.get("rdp_ips") or [])
if ip not in rdp:
rdp.append(ip)
d["rdp_ips"] = rdp
d["cluster_hostname"] = hostname
d["endpoint_id"] = ep.get("id")
d["endpoint_note"] = sys_note
d["user_note"] = user_note
if ep.get("model") and (d.get("source") == "inventory" or not d.get("model")):
pass # keep OME model authoritative for linked devices
if ep.get("switch_id") is not None:
d["endpoint_switch_id"] = ep.get("switch_id")
d["endpoint_switch_port"] = ep.get("switch_port")
_refresh_map_cidrs(d)
continue
# Skip synthetic if this IP already belongs to an OME device in the fleet
if ip in device_ips:
# Still attach notes onto the matching device if we can find it
for d in devices:
if ip in _device_ips(d) or d.get("ip") == ip or d.get("idrac_ip") == ip:
d["endpoint_id"] = ep.get("id")
d["endpoint_note"] = sys_note
d["user_note"] = user_note
break
continue
sid = _synthetic_device_id(ip)
if sid in existing_ids or sid in used_synth:
continue
used_synth.add(sid)
cidr = subnet_of(ip)
role = ep.get("role") or "server"
devices.append(
{
"id": sid,
"name": hostname,
"model": ep.get("model") or "Inventory host",
"service_tag": None,
"type": 1000,
"sub_type": "inventory",
"connected": True,
"power_state": None,
"powered_on": None,
"status": "inventory",
"ip": ip,
"idrac_ip": None,
"os_hostname": hostname,
"mgmt_dns_name": hostname,
"rdp_host": ip,
"rdp_ips": [ip],
"os_ips": [ip],
"extra_ips": [ip],
"is_windows": False,
"subnet": cidr,
"map_cidrs": [cidr],
"role": role,
"is_server": True,
"is_idrac": False,
"is_switch": role == "switch",
"is_chassis": False,
"is_pdu": role == "pdu",
"is_storage": role == "storage",
"chassis_service_tag": None,
"idrac_url": None,
"watts": None,
"source": "inventory",
"endpoint_id": ep.get("id"),
"endpoint_note": sys_note,
"user_note": user_note,
"endpoint_switch_id": ep.get("switch_id"),
"endpoint_switch_port": ep.get("switch_port"),
"cluster_hostname": hostname,
}
)
for d in devices:
_refresh_map_cidrs(d)
def build_subnet_summaries(devices: list[dict]) -> list[dict]:
"""Aggregate devices by map_cidrs (mgmt + secondary/inventory IPs) and attach VLAN catalog labels."""
catalog = _load_vlan_catalog_by_cidr()
buckets: dict[str, list[dict]] = defaultdict(list)
for d in devices:
cidrs = d.get("map_cidrs") or ([d.get("subnet")] if d.get("subnet") else [])
for c in cidrs:
if not c or c == "unknown":
continue
buckets[c].append(d)
for cidr in catalog:
buckets.setdefault(cidr, [])
subnets = []
for cidr, members in sorted(buckets.items(), key=lambda x: (-len(x[1]), x[0])):
seen: set = set()
uniq = []
for m in members:
mid = m.get("id")
if mid in seen:
continue
seen.add(mid)
uniq.append(m)
ids = [m["id"] for m in uniq if m.get("id") is not None]
sub_watts = sum(n["watts"] or 0 for n in uniq if n.get("watts") is not None)
switches = [n for n in uniq if n.get("is_switch")]
meta = catalog.get(cidr) or {}
subnets.append(
{
"cidr": cidr,
"count": len(uniq),
"connected": sum(1 for n in uniq if n.get("connected")),
"watts": round(sub_watts, 1) if sub_watts else None,
"device_ids": ids,
"has_switch": bool(switches),
"switches": [
{
"id": s["id"],
"name": s["name"],
"service_tag": s.get("service_tag"),
"ip": s.get("ip"),
}
for s in switches
],
"vlan_id": meta.get("vlan_id"),
"vlan_name": meta.get("name"),
"vlan_color": meta.get("color"),
"site_id": meta.get("site_id"),
}
)
return subnets
def _all_port_links() -> list[dict]:
with _db() as conn:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS port_links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
switch_id INTEGER NOT NULL,
switch_port INTEGER NOT NULL,
device_id INTEGER NOT NULL,
device_port TEXT,
note TEXT,
updated_at REAL NOT NULL,
UNIQUE(switch_id, switch_port)
)
"""
)
return [dict(r) for r in conn.execute("SELECT * FROM port_links").fetchall()]
_NETWORK_INV_TS = 0.0
def ensure_network_inventory_in_state(force: bool = False) -> None:
"""Merge inventory endpoints into live fleet STATE (safe between OME polls)."""
global _NETWORK_INV_TS
now = time.time()
if not force and STATE.get("devices") and (now - _NETWORK_INV_TS) < 8:
return
devices = list(STATE.get("devices") or [])
sync_endpoints_from_fleet(devices)
devices = [d for d in devices if d.get("source") != "inventory"]
apply_network_endpoints(devices)
STATE["devices"] = devices
STATE["subnets"] = build_subnet_summaries(devices)
_NETWORK_INV_TS = now
def build_vlan_map() -> dict:
"""VLANs from catalog + membership from fleet IPs + inventory endpoints; switch attachment matrix."""
_seed_atc_vlans()
_seed_network_endpoints()
_migrate_rack_items()
ensure_network_inventory_in_state()
devices = list(STATE.get("devices") or [])
with _db() as conn:
rows = [dict(r) for r in conn.execute("SELECT * FROM vlans ORDER BY sort_order, vlan_id").fetchall()]
def in_cidr(ip: str, cidr: str) -> bool:
try:
return ipaddress.ip_address(ip) in ipaddress.ip_network(cidr, strict=False)
except Exception:
return False
switches_by_id = {d.get("id"): d for d in devices if d.get("is_switch")}
links = _all_port_links()
links_by_device: dict[int, list[dict]] = defaultdict(list)
for link in links:
did = link.get("device_id")
if did is None:
continue
sw = switches_by_id.get(link.get("switch_id")) or {}
links_by_device[int(did)].append(
{
"switch_id": link.get("switch_id"),
"switch_name": sw.get("name") or f"switch-{link.get('switch_id')}",
"switch_ip": sw.get("ip") or sw.get("idrac_ip"),
"switch_port": link.get("switch_port"),
"device_port": link.get("device_port") or "",
"note": link.get("note") or "",
}
)
for d in devices:
sid = d.get("endpoint_switch_id")
if sid is None:
continue
did = d.get("id")
if did is None:
continue
already = {(x["switch_id"], x.get("switch_port")) for x in links_by_device.get(did, [])}
port = d.get("endpoint_switch_port")
key = (sid, port)
if key in already:
continue
sw = switches_by_id.get(sid) or {}
links_by_device[int(did)].append(
{
"switch_id": sid,
"switch_name": sw.get("name") or f"switch-{sid}",
"switch_ip": sw.get("ip") or sw.get("idrac_ip"),
"switch_port": port,
"device_port": "",
"note": "from inventory endpoint",
"inferred": True,
}
)
eps_by_ip = {e.get("ip"): e for e in _load_network_endpoints() if e.get("ip")}
vlans = []
for v in rows:
members = []
for d in devices:
matched = [ip for ip in _device_ips(d) if in_cidr(ip, v["cidr"])]
if not matched:
continue
mid = d.get("id")
display = d.get("cluster_hostname") or d.get("name")
ep = None
for ip in matched:
if ip in eps_by_ip:
ep = eps_by_ip[ip]
break
model = d.get("model")
if d.get("source") == "inventory" and ep and ep.get("model"):
model = ep.get("model")
members.append(
{
"id": mid,
"name": display,
"ome_name": d.get("name"),
"service_tag": d.get("service_tag"),
"role": d.get("role"),
"connected": d.get("connected"),
"ips": matched,
"model": model,
"source": d.get("source") or "ome",
"switch_ports": links_by_device.get(mid, []),
"endpoint_id": d.get("endpoint_id") or (ep or {}).get("id"),
"endpoint_note": d.get("endpoint_note") or (ep or {}).get("note") or "",
"user_note": d.get("user_note") or (ep or {}).get("user_note") or "",
}
)
members.sort(key=lambda m: (m.get("name") or "").lower())
vlans.append(
{
**v,
"member_count": len(members),
"connected_count": sum(1 for m in members if m.get("connected")),
"members": members[:250],
}
)
matrix = []
for d in devices:
if not (d.get("is_server") or d.get("source") == "inventory"):
continue
if d.get("is_switch") or d.get("is_pdu"):
continue
ips = _device_ips(d)
member_vlans = []
for v in rows:
matched = [ip for ip in ips if in_cidr(ip, v["cidr"])]
if matched:
member_vlans.append(
{
"vlan_id": v.get("vlan_id"),
"name": v.get("name"),
"cidr": v.get("cidr"),
"color": v.get("color"),
"ips": matched,
}
)
if not member_vlans and not links_by_device.get(d.get("id")):
continue
matrix.append(
{
"id": d.get("id"),
"name": d.get("cluster_hostname") or d.get("name"),
"ome_name": d.get("name"),
"service_tag": d.get("service_tag"),
"model": d.get("model"),
"role": d.get("role"),
"connected": d.get("connected"),
"source": d.get("source") or "ome",
"ips": ips,
"vlans": member_vlans,
"switch_ports": links_by_device.get(d.get("id"), []),
"endpoint_id": d.get("endpoint_id"),
"endpoint_note": d.get("endpoint_note") or "",
"user_note": d.get("user_note") or "",
}
)
matrix.sort(key=lambda m: (m.get("name") or "").lower())
seen = {v["cidr"] for v in rows}
discovered = []
buckets: dict[str, int] = {}
for d in devices:
for ip in _device_ips(d):
if ip.count(".") != 3:
continue
cidr = ".".join(ip.split(".")[:3]) + ".0/24"
if cidr.startswith("127."):
continue
buckets[cidr] = buckets.get(cidr, 0) + 1
for cidr, cnt in sorted(buckets.items(), key=lambda x: -x[1]):
if cidr not in seen:
discovered.append({"cidr": cidr, "device_ips": cnt, "in_catalog": False})
return {
"vlans": vlans,
"attachment_matrix": matrix,
"port_links": [
{
"switch_id": l.get("switch_id"),
"switch_port": l.get("switch_port"),
"device_id": l.get("device_id"),
"device_port": l.get("device_port"),
"note": l.get("note"),
}
for l in links
],
"discovered_subnets": discovered,
"note": "VLAN catalog + OME IPs + inventory endpoints. Switch ports from Fabric wiring and endpoint hints.",
}
def _migrate_rack_items() -> None:
"""Copy legacy rack_placements into flexible rack_items once."""
with _db() as conn:
n = conn.execute("SELECT COUNT(*) AS c FROM rack_items").fetchone()["c"]
if n > 0:
return
try:
rows = conn.execute("SELECT * FROM rack_placements").fetchall()
except Exception:
return
now = time.time()
for r in rows:
conn.execute(
"""
INSERT INTO rack_items(rack_id, device_id, catalog_sku, label, u_start, u_height, side, notes, updated_at)
VALUES (?, ?, NULL, NULL, ?, ?, 'front', NULL, ?)
""",
(r["rack_id"], r["device_id"], r["u_start"], r["u_height"], now),
)
if rows:
log.info("Migrated %s rack_placements → rack_items", len(rows))
OPS_USERS = [
{
"id": "jody",
"name": "Jody van Dongen",
"role": "ATC Datacenter Admin",
"team": "admin",
"email": "jody.van.dongen@dell.com",
"focus": "OME fleet · racks · warranty / compliance",
},
{
"id": "laurens",
"name": "Laurens Rammers",
"role": "ATC Datacenter Admin",
"team": "admin",
"email": "laurens.rammers@dell.com",
"focus": "Datacenter ops · handoffs · escalation",
},
{
"id": "mo",
"name": "Mohamed El Kadi",
"role": "Data Forward Deployed Engineer",
"team": "fde",
"email": "mohamed.el.kadi@dell.com",
"focus": "OME Cockpit · OpenManage AI · AI workloads on FDE cluster",
},
{
"id": "bart",
"name": "Bart Sjerps",
"role": "Data Forward Deployed Engineer",
"team": "fde",
"email": "bart.sjerps@dell.com",
"focus": "FDE cluster · AI workload deployment with Mo",
},
]
# Back-compat alias used by older ticket endpoints / UI
ADMINS = OPS_USERS
def _ops_user(uid: str) -> dict | None:
for u in OPS_USERS:
if u["id"] == uid:
return u
return None
def _default_assignee(created_by: str) -> str:
"""Admins hand off to each other; FDE hand off to an admin by default."""
if created_by == "jody":
return "laurens"
if created_by == "laurens":
return "jody"
if created_by in ("mo", "bart"):
return "jody"
return "jody"
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 _backup_ops_db(reason: str = "manual") -> Path | None:
"""Snapshot ops.db so rebuilds / wipes do not silently lose ticket history."""
try:
if not DB_PATH.exists() or DB_PATH.stat().st_size < 100:
return None
bak_dir = DATA_DIR / "backups"
bak_dir.mkdir(parents=True, exist_ok=True)
stamp = time.strftime("%Y%m%d-%H%M%S")
dest = bak_dir / f"ops-{stamp}-{reason}-{int(time.time()*1000)%100000}.db"
import shutil
shutil.copy2(DB_PATH, dest)
# keep last 20 backups
olds = sorted(bak_dir.glob("ops-*.db"), key=lambda x: x.stat().st_mtime, reverse=True)
for stale in olds[20:]:
try:
stale.unlink()
except Exception:
pass
return dest
except Exception as e:
log.warning("ops.db backup failed: %s", e)
return None
def _restore_ops_db_if_empty() -> bool:
"""If tickets table is empty but a backup exists, restore the newest backup with tickets."""
try:
with _db() as conn:
n = conn.execute("SELECT COUNT(*) AS c FROM tickets").fetchone()["c"]
if n > 0:
return False
bak_dir = DATA_DIR / "backups"
if not bak_dir.exists():
return False
candidates = sorted(bak_dir.glob("ops-*.db"), key=lambda x: x.stat().st_mtime, reverse=True)
for cand in candidates:
try:
c2 = sqlite3.connect(cand)
c2.row_factory = sqlite3.Row
cnt = c2.execute("SELECT COUNT(*) AS c FROM tickets").fetchone()["c"]
c2.close()
if cnt <= 0:
continue
import shutil
# preserve empty current as .empty
if DB_PATH.exists():
shutil.copy2(DB_PATH, bak_dir / f"ops-empty-before-restore-{int(time.time())}.db")
shutil.copy2(cand, DB_PATH)
log.warning("Restored ops.db from backup %s (%s tickets)", cand.name, cnt)
return True
except Exception as e:
log.debug("skip backup %s: %s", cand, e)
return False
except Exception as e:
log.warning("ops.db restore check failed: %s", e)
return False
def init_db():
DATA_DIR.mkdir(parents=True, exist_ok=True)
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")
conn.executescript(
"""
CREATE TABLE IF NOT EXISTS sites (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
sort_order INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS racks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
site_id TEXT NOT NULL REFERENCES sites(id),
name TEXT NOT NULL,
units INTEGER NOT NULL DEFAULT 42,
sort_order INTEGER NOT NULL,
created_at REAL NOT NULL,
updated_at REAL NOT NULL
);
CREATE TABLE IF NOT EXISTS rack_placements (
device_id INTEGER PRIMARY KEY,
rack_id INTEGER NOT NULL REFERENCES racks(id) ON DELETE CASCADE,
u_start INTEGER NOT NULL,
u_height INTEGER NOT NULL,
updated_at REAL NOT NULL
);
CREATE TABLE IF NOT EXISTS rack_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rack_id INTEGER NOT NULL REFERENCES racks(id) ON DELETE CASCADE,
device_id INTEGER,
catalog_sku TEXT,
label TEXT,
u_start INTEGER NOT NULL,
u_height INTEGER NOT NULL,
side TEXT NOT NULL DEFAULT 'front',
notes TEXT,
updated_at REAL NOT NULL
);
CREATE TABLE IF NOT EXISTS port_links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
switch_id INTEGER NOT NULL,
switch_port INTEGER NOT NULL,
device_id INTEGER NOT NULL,
device_port TEXT,
note TEXT,
updated_at REAL NOT NULL,
UNIQUE(switch_id, switch_port)
);
CREATE TABLE IF NOT EXISTS vlans (
id INTEGER PRIMARY KEY AUTOINCREMENT,
vlan_id INTEGER,
name TEXT NOT NULL,
cidr TEXT NOT NULL,
purpose TEXT,
site_id TEXT,
color TEXT,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at REAL NOT NULL,
updated_at REAL NOT NULL
);
CREATE TABLE IF NOT EXISTS network_endpoints (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hostname TEXT,
ip TEXT NOT NULL UNIQUE,
role TEXT NOT NULL DEFAULT 'server',
kind TEXT NOT NULL DEFAULT 'physical',
device_id INTEGER,
vlan_id INTEGER,
switch_id INTEGER,
switch_port INTEGER,
model TEXT,
note TEXT,
created_at REAL NOT NULL,
updated_at REAL NOT NULL
);
"""
)
_restore_ops_db_if_empty()
_seed_atc_racks()
_seed_atc_vlans()
_seed_network_endpoints()
n = 0
try:
with _db() as conn:
n = conn.execute("SELECT COUNT(*) AS c FROM tickets").fetchone()["c"]
except Exception:
pass
log.info("Ops tickets DB ready at %s (%s tickets)", DB_PATH, n)
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()
_backup_ops_db("startup")
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/memory 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)
# Wait until fleet snapshot exists, then warm memory inventory for chat tools
for _ in range(40):
if STATE.get("devices"):
break
await asyncio.sleep(2)
try:
mem = await fetch_fleet_memory(force=False)
log.info(
"Memory cache warmed: %s/%s servers",
mem.get("with_memory"),
mem.get("count"),
)
except Exception as e:
log.warning("Memory 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():
ensure_network_inventory_in_state()
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,
}
detail["expansion"] = build_expansion_report(node.get("model"), detail.get("inventory") or {})
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.get("/api/devices/{device_id}/expansion")
async def device_expansion(device_id: int, force: bool = False):
if force and device_id in DETAIL_CACHE:
DETAIL_CACHE.pop(device_id, None)
detail = await device_detail(device_id)
return {
"device": detail.get("device"),
"expansion": detail.get("expansion")
or build_expansion_report((detail.get("device") or {}).get("model"), detail.get("inventory") or {}),
}
@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 _device_by_id(device_id: int | None) -> dict | None:
if device_id is None:
return None
return next((d for d in (STATE.get("devices") or []) if d.get("id") == device_id), None)
def _device_by_service_tag(st: str) -> dict | None:
needle = (st or "").strip().upper()
if not needle:
return None
for d in STATE.get("devices") or []:
if (d.get("service_tag") or "").strip().upper() == needle:
return d
return None
def _find_devices_fuzzy(query: str, limit: int = 8) -> list[dict]:
q = (query or "").strip().lower()
if not q:
return []
hits = []
for d in STATE.get("devices") or []:
blob = " ".join(
str(d.get(k) or "").lower()
for k in ("name", "service_tag", "ip", "idrac_ip", "rdp_host", "os_hostname", "model")
)
if q in blob:
hits.append(d)
hits.sort(key=lambda d: (0 if (d.get("service_tag") or "").lower() == q else 1, d.get("name") or ""))
return hits[:limit]
def _format_device_card(d: dict) -> str:
mem = d.get("memory_gb")
mem_s = f"{mem}GB/{d.get('dimm_count') or '?'}DIMM" if mem is not None else "unknown"
return (
"ST={st} | name={name} | model={model} | idrac={idrac} | "
"rdp={rdp} | os_host={osh} | ram={ram} | connected={conn} | status={status} | watts={watts}"
).format(
st=(d.get("service_tag") or "NONE"),
name=(d.get("name") or "")[:48],
model=(d.get("model") or "")[:36],
idrac=d.get("idrac_ip") or d.get("ip") or "—",
rdp=(",".join(d.get("rdp_ips") or []) or d.get("rdp_host") or "unresolved"),
osh=d.get("os_hostname") or "—",
ram=mem_s,
conn="yes" if d.get("connected") else "no",
status=d.get("status"),
watts=d.get("watts"),
)
async def ome_fetch_inventory_types(device_id: int, types: list[str]) -> dict[str, list]:
"""Fast parallel inventory fetch for chat tools (not full device landscape)."""
inventory: dict[str, list] = {}
async with httpx.AsyncClient(verify=False, timeout=45.0) as client:
base, headers, sid = await ome_session(client)
try:
async def one(inv_type: str):
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:
return inv_type, info
except Exception as e:
log.debug("chat inv %s failed: %s", inv_type, e)
return inv_type, []
results = await asyncio.gather(*[one(t) for t in types])
for inv_type, info in results:
if info:
inventory[inv_type] = info
finally:
await ome_session_delete(client, base, headers, sid)
return inventory
def _memory_from_dimms(mem_raw: list) -> dict:
"""Compact CURRENT memory summary from OME serverMemoryDevices."""
dimms = 0
total_gb = 0.0
for dimm in mem_raw or []:
gb = _mem_size_gb(dimm.get("Size"))
if gb is None:
continue
dimms += 1
total_gb += gb
return {
"memory_gb": round(total_gb, 1) if dimms else None,
"dimm_count": dimms or None,
}
async def fetch_fleet_memory(force: bool = False) -> dict:
"""Warm/cache CURRENT installed RAM for all managed servers (OME inventory)."""
cached = None if force else _cache_get("fleet_memory")
if cached is not None:
return cached
devices = [
d
for d in (STATE.get("devices") or [])
if (d.get("is_server") or d.get("is_idrac") or (d.get("type") == 1000))
and d.get("source") != "inventory"
and isinstance(d.get("id"), int)
and d.get("id") > 0
]
# Prefer unique device ids
seen: set[int] = set()
targets: list[dict] = []
for d in devices:
did = d.get("id")
if did is None or did in seen:
continue
seen.add(int(did))
targets.append(d)
rows: list[dict] = []
errors = 0
sem = asyncio.Semaphore(10)
async with httpx.AsyncClient(verify=False, timeout=60.0) as client:
base, headers, sid = await ome_session(client)
try:
async def one(node: dict):
nonlocal errors
did = int(node["id"])
async with sem:
try:
ir = await client.get(
f"{base}/api/DeviceService/Devices({did})/InventoryDetails('serverMemoryDevices')",
headers=headers,
)
mem_raw = []
if ir.status_code == 200:
mem_raw = ir.json().get("InventoryInfo") or []
stats = _memory_from_dimms(mem_raw)
return {
"id": did,
"service_tag": node.get("service_tag"),
"name": node.get("name"),
"model": node.get("model"),
"idrac_ip": node.get("idrac_ip") or node.get("ip"),
"connected": bool(node.get("connected")),
"memory_gb": stats.get("memory_gb"),
"dimm_count": stats.get("dimm_count"),
}
except Exception as e:
errors += 1
log.debug("fleet memory %s: %s", did, e)
return {
"id": did,
"service_tag": node.get("service_tag"),
"name": node.get("name"),
"model": node.get("model"),
"idrac_ip": node.get("idrac_ip") or node.get("ip"),
"connected": bool(node.get("connected")),
"memory_gb": None,
"dimm_count": None,
"error": str(e)[:80],
}
rows = list(await asyncio.gather(*[one(n) for n in targets]))
finally:
await ome_session_delete(client, base, headers, sid)
rows.sort(key=lambda r: ((r.get("name") or "").lower(), r.get("service_tag") or ""))
known = [r for r in rows if r.get("memory_gb") is not None]
payload = {
"source": "OME DeviceService InventoryDetails(serverMemoryDevices)",
"count": len(rows),
"with_memory": len(known),
"errors": errors,
"total_memory_gb": round(sum(r.get("memory_gb") or 0 for r in known), 1),
"servers": rows,
"updated_at": time.time(),
}
_cache_set("fleet_memory", payload)
# also stamp onto fleet nodes for UI/context
by_id = {r["id"]: r for r in rows if r.get("id") is not None}
for d in STATE.get("devices") or []:
hit = by_id.get(d.get("id"))
if hit:
d["memory_gb"] = hit.get("memory_gb")
d["dimm_count"] = hit.get("dimm_count")
log.info(
"Fleet memory cache: %s/%s servers with RAM (%.0f GB total)",
len(known),
len(rows),
payload["total_memory_gb"],
)
return payload
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 {}
by_id = {d.get("id"): d for d in devices if d.get("id") is not None}
st_lines = [
"SERVICE TAG INDEX (canonical). idrac=BMC management IP. rdp=OS/Windows RDP candidates (DNS). Never call idrac the RDP IP.",
]
for d in sorted(devices, key=lambda x: (x.get("name") or "").lower()):
st = (d.get("service_tag") or "").strip() or "NONE"
rdp = ",".join((d.get("rdp_ips") or [])[:2]) or (d.get("rdp_host") or "—")
mem = d.get("memory_gb")
ram = f"{mem}GB" if mem is not None else "?"
st_lines.append(
"- ST={st} | {name} | idrac={idrac} | rdp={rdp} | model={model} | ram={ram} | connected={conn}".format(
st=st,
name=(d.get("name") or "")[:36],
idrac=d.get("idrac_ip") or d.get("ip") or "—",
rdp=rdp[:40],
model=(d.get("model") or "")[:26],
ram=ram,
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.",
"ATC Datacenter Admins (escalate here when facts are missing):",
" - Jody van Dongen <jody.van.dongen@dell.com>",
" - Laurens Rammers <laurens.rammers@dell.com>",
"ACCURACY RULES (mandatory — never break these):",
"1) Use ONLY facts from this snapshot and any TOOL FACTS block. Never invent Service Tags, IPs, DIMM counts, firmware versions, RDP targets, port maps, VLAN members, or rack placements.",
"2) If a requested fact is not present in the snapshot/TOOL FACTS, or tools failed/returned empty: say exactly what is unknown, then tell the user to overleggen met Jody van Dongen and Laurens Rammers (emails above). Do not guess.",
"3) ALWAYS cite Service Tag (ST=...), model, idrac IP, and rdp IP (or 'rdp unresolved') when discussing a system.",
"4) Management/iDRAC IP is NOT the Windows RDP IP. Never recommend RDP to an iDRAC address.",
"5) Hardware CURRENT (OME inventory) is NOT Dell CATALOG MAX. Never state catalog maxima as installed capacity.",
"6) Prefer TOOL FACTS over the summary index when both are present. Quote TOOL FACTS numbers verbatim.",
"7) Never pretend MCP/tool output exists when no TOOL FACTS block was provided.",
"",
"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 summary (Dell 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],
)
)
mem_cache = _cache_get("fleet_memory") or REPORT_CACHE.get("fleet_memory") or {}
mem_servers = mem_cache.get("servers") or []
if mem_servers:
lines.append(
"MEMORY INDEX (CURRENT OME inventory, GB installed): known={k}/{n} total_gb={tg}".format(
k=mem_cache.get("with_memory"),
n=mem_cache.get("count"),
tg=mem_cache.get("total_memory_gb"),
)
)
ranked = sorted(
[r for r in mem_servers if r.get("memory_gb") is not None],
key=lambda r: (0 if r.get("connected") else 1, (r.get("name") or "").lower()),
)
for r in ranked[:55]:
lines.append(
"- ST={st} {name} model={model} ram={gb}GB dimms={dc} idrac={ip}".format(
st=r.get("service_tag") or "NONE",
name=(r.get("name") or "")[:28],
model=(r.get("model") or "")[:22],
gb=r.get("memory_gb"),
dc=r.get("dimm_count") or "?",
ip=r.get("idrac_ip") or "—",
)
)
if len(ranked) > 55:
lines.append(f"- … +{len(ranked) - 55} more in TOOL FACTS list_fleet_memory")
else:
lines.append(
"MEMORY INDEX: not warmed yet — call list_fleet_memory tool / wait for background warm."
)
lines.append("CONNECTED (sample):")
for d in connected[:14]:
lines.append("- " + _format_device_card(d))
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"),
)
)
def _alert_line(a: dict, msg_len: int) -> str:
node = by_id.get(a.get("device_id")) or {}
st = node.get("service_tag") or a.get("service_tag") or "NONE"
return "- ST={st} {dev} idrac={ip}: {msg}".format(
st=st,
dev=(a.get("device") or node.get("name") or "")[:28],
ip=a.get("ip") or node.get("idrac_ip") or node.get("ip") or "—",
msg=(a.get("message") or "")[:msg_len],
)
lines.append("CRITICAL:")
if not critical:
lines.append("(none)")
for a in critical:
lines.append(_alert_line(a, 100))
lines.append("WARNINGS:")
for a in warnings:
lines.append(_alert_line(a, 80))
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:
node = by_id.get(h.get("id")) or {}
lines.append(
"- ST={st} {name}: {watts}W".format(
st=(node.get("service_tag") or h.get("service_tag") or "NONE"),
name=(h.get("name") or node.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 = _device_by_id(focus_device_id)
lines.append("FOCUS DEVICE:")
if node:
lines.append(_format_device_card(node))
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 from fleet snapshot" % focus_device_id)
lines.append(
"Reply with concrete Service Tags, idrac/rdp IPs, models, and next actions. "
"Quote TOOL FACTS verbatim for hardware numbers. "
"If anything is missing from facts: do not invent — escalate to Jody van Dongen and Laurens Rammers."
)
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
def plan_chat_tools(message: str, focus_device_id: int | None = None) -> list[tuple[str, dict]]:
"""Server-side tool planner — Llama3-GPTQ does not emit native tool_calls reliably."""
import re
msg = message or ""
low = msg.lower()
planned: list[tuple[str, dict]] = []
seen: set[str] = set()
def add(name: str, args: dict):
key = name + json.dumps(args, sort_keys=True, default=str)
if key in seen:
return
seen.add(key)
planned.append((name, args))
for st in re.findall(r"\b([A-Z0-9]{7})\b", msg.upper()):
if st in {"OMEPROD", "WINDOWS", "POWERED", "UNKNOWN", "SERVICE", "CONNECTED"}:
continue
if _device_by_service_tag(st):
add("lookup_device", {"query": st})
for m in re.finditer(r"\b(?:for|on|about|host|server|node)\s+([A-Za-z0-9._-]{4,40})", msg, re.I):
add("lookup_device", {"query": m.group(1)})
wants_hw = bool(
re.search(
r"\b(memory|ram|dimm|disk|drive|ssd|hdd|pcie|expansion|cpu|processor|core|slot|bay|installed|capacity)\b",
low,
)
)
wants_rdp = bool(re.search(r"\b(rdp|remote\s*desktop|windows|hyper-?v|os\s*ip|hostname)\b", low))
wants_fw = bool(re.search(r"\b(firmware|bios\s*version|outdated|compliance|firmware\s*catalog|update\s*catalog|baseline)\b", low))
wants_war = bool(re.search(r"\b(warrant|prosupport|support\s*end|days?\s*left)\b", low))
wants_alerts = bool(re.search(r"\b(alert|critical|warning|fault|health)\b", low))
wants_counts = bool(
re.search(
r"\b(hoeveel|how\s+many|count|aantal|total|fleet\s+size|connected|offline)\b",
low,
)
)
# Fleet summary questions still benefit from alerts + memory tools when relevant
if wants_counts and not planned:
add("list_alerts", {})
target_id = focus_device_id
target_st = None
if planned:
q0 = planned[0][1].get("query")
node = _device_by_service_tag(str(q0)) or ((_find_devices_fuzzy(str(q0), 1) or [None])[0])
if node:
target_id = node.get("id")
target_st = node.get("service_tag")
if wants_rdp and not any(n == "lookup_device" for n, _ in planned):
if focus_device_id:
node = _device_by_id(focus_device_id)
if node:
add("lookup_device", {"query": node.get("service_tag") or node.get("name") or str(focus_device_id)})
else:
n_lookups = 0
for d in STATE.get("devices") or []:
if d.get("is_windows") or d.get("rdp_host"):
add("lookup_device", {"query": d.get("service_tag") or d.get("name")})
n_lookups += 1
if n_lookups >= 4:
break
wants_fleet_mem = bool(
re.search(
r"\b(each|every|all|per\s+server|fleet|overview|elk|alle|iedere|elke)\b",
low,
)
) or bool(re.search(r"hoeveel\s+(memory|ram|geheugen)", low))
if wants_hw:
args: dict = {}
if target_id:
args["device_id"] = int(target_id)
if target_st:
args["service_tag"] = target_st
if args:
add("get_hardware", args)
if wants_fleet_mem:
add("list_fleet_memory", {})
elif wants_fleet_mem or not focus_device_id:
add("list_fleet_memory", {})
elif focus_device_id:
add("get_hardware", {"device_id": int(focus_device_id)})
if wants_fw:
args = {"outdated_only": True}
if target_id:
args["device_id"] = int(target_id)
if target_st:
args["service_tag"] = target_st
add("get_compliance", args)
if wants_war:
args = {}
if target_id:
args["device_id"] = int(target_id)
if target_st:
args["service_tag"] = target_st
if args:
add("get_warranty", args)
elif focus_device_id:
add("get_warranty", {"device_id": int(focus_device_id)})
if wants_alerts:
args = {}
if target_id:
args["device_id"] = int(target_id)
if target_st:
args["service_tag"] = target_st
add("list_alerts", args)
if focus_device_id and not planned:
node = _device_by_id(focus_device_id)
if node:
add("lookup_device", {"query": node.get("service_tag") or str(focus_device_id)})
return planned[:6]
async def run_chat_tool(name: str, args: dict) -> dict:
args = args or {}
try:
if name == "lookup_device":
q = str(args.get("query") or "").strip()
exact = _device_by_service_tag(q)
hits = [exact] if exact else _find_devices_fuzzy(q, 8)
return {
"tool": name,
"query": q,
"count": len(hits),
"devices": [
{
"id": d.get("id"),
"service_tag": d.get("service_tag"),
"name": d.get("name"),
"model": d.get("model"),
"idrac_ip": d.get("idrac_ip") or d.get("ip"),
"rdp_host": d.get("rdp_host"),
"rdp_ips": d.get("rdp_ips") or [],
"os_hostname": d.get("os_hostname"),
"is_windows": d.get("is_windows"),
"connected": d.get("connected"),
"status": d.get("status"),
"watts": d.get("watts"),
}
for d in hits
if d
],
}
if name == "get_hardware":
node = None
if args.get("device_id") is not None:
node = _device_by_id(int(args["device_id"]))
if not node and args.get("service_tag"):
node = _device_by_service_tag(str(args["service_tag"]))
if not node:
return {"tool": name, "error": "device not found in fleet snapshot"}
did = int(node["id"])
inv = await ome_fetch_inventory_types(
did,
[
"serverMemoryDevices",
"serverProcessors",
"serverArrayDisks",
"serverRaidControllers",
"serverDeviceCards",
"serverOperatingSystems",
],
)
report = build_expansion_report(node.get("model"), inv)
cur = report.get("current") or {}
exp = report.get("expansion") or {}
return {
"tool": name,
"device": {
"id": did,
"service_tag": node.get("service_tag"),
"name": node.get("name"),
"model": node.get("model"),
"idrac_ip": node.get("idrac_ip") or node.get("ip"),
"rdp_ips": node.get("rdp_ips") or [],
"os_hostname": node.get("os_hostname"),
},
"current_ome_inventory": {
"memory_installed_gb": (cur.get("memory") or {}).get("installed_gb"),
"dimm_count": (cur.get("memory") or {}).get("dimm_count"),
"dimm_modules": [
{
"slot": m.get("slot"),
"size_gb": m.get("size_gb"),
"rated_mts": m.get("rated_mts"),
"operating_mts": m.get("operating_mts"),
}
for m in ((cur.get("memory") or {}).get("modules") or [])[:24]
],
"cpu_sockets_populated": (cur.get("cpu") or {}).get("sockets_populated"),
"cpu_cores_total": (cur.get("cpu") or {}).get("cores_total"),
"processors": (cur.get("cpu") or {}).get("processors") or [],
"disk_count": (cur.get("disks") or {}).get("count"),
"disk_capacity_gb": (cur.get("disks") or {}).get("capacity_gb"),
"disks": [
{
"bay": d.get("bay"),
"model": d.get("model"),
"size_gb": d.get("size_gb"),
"bus": d.get("bus"),
"media": d.get("media"),
}
for d in ((cur.get("disks") or {}).get("items") or [])[:20]
],
"os": cur.get("os") or [],
},
"catalog_max_not_measured": {
"source": exp.get("source"),
"catalog_known": exp.get("catalog_known"),
"catalog_model": exp.get("catalog_model"),
"memory_max_tb": (exp.get("memory") or {}).get("max_capacity_tb"),
"dimm_slots_max": (exp.get("memory") or {}).get("dimm_slots_max"),
"cpu_sockets_max": (exp.get("cpu") or {}).get("sockets_max"),
"drive_bays_max": (exp.get("disks") or {}).get("bays_max"),
"pcie_slots_max": (exp.get("pcie") or {}).get("slots_max"),
},
"findings": report.get("findings") or [],
}
if name == "list_fleet_memory":
mem = await fetch_fleet_memory(force=bool(args.get("force")))
servers = mem.get("servers") or []
# Compact rows for the LLM — include unknown so the model does not invent
slim = [
{
"service_tag": s.get("service_tag"),
"name": s.get("name"),
"model": s.get("model"),
"idrac_ip": s.get("idrac_ip"),
"connected": s.get("connected"),
"memory_gb": s.get("memory_gb"),
"dimm_count": s.get("dimm_count"),
}
for s in servers
]
return {
"tool": name,
"source": mem.get("source"),
"count": mem.get("count"),
"with_memory": mem.get("with_memory"),
"total_memory_gb": mem.get("total_memory_gb"),
"note": "memory_gb is CURRENT installed RAM from OME inventory (not catalog max).",
"servers": slim,
}
if name == "get_compliance":
compliance = await fetch_compliance(force=False)
comps = compliance.get("components") or []
node = None
if args.get("device_id") is not None:
node = _device_by_id(int(args["device_id"]))
if not node and args.get("service_tag"):
node = _device_by_service_tag(str(args["service_tag"]))
outdated_only = bool(args.get("outdated_only", True))
if node:
did = node.get("id")
st = (node.get("service_tag") or "").upper()
rows = [
c
for c in comps
if c.get("device_id") == did
or (st and (c.get("service_tag") or "").upper() == st)
]
else:
rows = list(comps)
if outdated_only:
rows = [
c
for c in rows
if str(c.get("compliance_status") or "").lower()
in ("critical", "warning", "downgrade", "non-compliant", "outdated")
or (
str(c.get("update_action") or "").lower()
not in ("", "equal", "compliant", "ok", "none")
and str(c.get("current_version") or "") != str(c.get("catalog_version") or "")
)
]
slim = [
{
"service_tag": c.get("service_tag"),
"device_name": c.get("device_name"),
"component": c.get("component"),
"current_version": c.get("current_version"),
"catalog_version": c.get("catalog_version"),
"compliance_status": c.get("compliance_status"),
"update_action": c.get("update_action"),
}
for c in rows[:40]
]
return {"tool": name, "summary": compliance.get("summary"), "count": len(slim), "components": slim}
if name == "get_warranty":
warranties = await fetch_warranties(force=False)
items = warranties.get("items") or warranties.get("warranties") or []
node = None
if args.get("device_id") is not None:
node = _device_by_id(int(args["device_id"]))
if not node and args.get("service_tag"):
node = _device_by_service_tag(str(args["service_tag"]))
if not node:
return {"tool": name, "error": "device not found"}
st = (node.get("service_tag") or "").upper()
rows = [
w
for w in items
if w.get("device_id") == node.get("id")
or (st and (w.get("service_tag") or "").upper() == st)
]
return {
"tool": name,
"service_tag": node.get("service_tag"),
"name": node.get("name"),
"warranties": rows[:8],
}
if name == "list_alerts":
alerts = STATE.get("alerts") or []
by_id = {d.get("id"): d for d in (STATE.get("devices") or [])}
node = None
if args.get("device_id") is not None:
node = _device_by_id(int(args["device_id"]))
if not node and args.get("service_tag"):
node = _device_by_service_tag(str(args["service_tag"]))
sev = (args.get("severity") or "").strip().lower()
out = []
for a in alerts:
if node and a.get("device_id") != node.get("id"):
continue
if sev and str(a.get("severity") or "").lower() != sev:
continue
d = by_id.get(a.get("device_id")) or {}
out.append(
{
"severity": a.get("severity"),
"service_tag": d.get("service_tag"),
"device": a.get("device") or d.get("name"),
"idrac_ip": a.get("ip") or d.get("idrac_ip") or d.get("ip"),
"message": (a.get("message") or "")[:180],
}
)
if len(out) >= 25:
break
return {"tool": name, "count": len(out), "alerts": out}
return {"tool": name, "error": f"unknown tool {name}"}
except Exception as e:
log.exception("chat tool %s failed", name)
return {"tool": name, "error": str(e)}
def format_tool_facts(results: list[dict]) -> str:
if not results:
return ""
parts = ["TOOL FACTS (live lookups — authoritative; do not contradict):"]
for r in results:
if r.get("tool") == "list_fleet_memory" and r.get("servers"):
lines = [
"list_fleet_memory CURRENT RAM:",
f"known={r.get('with_memory')}/{r.get('count')} total_gb={r.get('total_memory_gb')}",
"ST\tname\tmodel\tmemory_gb\tdimms\tidrac",
]
for s in r.get("servers") or []:
lines.append(
"{st}\t{name}\t{model}\t{gb}\t{dc}\t{ip}".format(
st=s.get("service_tag") or "NONE",
name=(s.get("name") or "")[:36],
model=(s.get("model") or "")[:24],
gb=s.get("memory_gb") if s.get("memory_gb") is not None else "unknown",
dc=s.get("dimm_count") if s.get("dimm_count") is not None else "unknown",
ip=s.get("idrac_ip") or "—",
)
)
parts.append("\n".join(lines)[:14000])
else:
parts.append(json.dumps(r, ensure_ascii=False, default=str)[:14000])
return "\n".join(parts)
@app.get("/api/fleet-memory")
async def api_fleet_memory(force: bool = False):
"""CURRENT installed RAM per server (OME inventory cache)."""
return await fetch_fleet_memory(force=force)
@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": OPS_USERS, "users": OPS_USERS}
@app.get("/api/ops/users")
async def api_ops_users():
return {
"users": OPS_USERS,
"admins": [u for u in OPS_USERS if u.get("team") == "admin"],
"fde": [u for u in OPS_USERS if u.get("team") == "fde"],
"context": {
"cockpit": "OME Cockpit by Data Forward Deployed Engineers Mohamed El Kadi & Bart Sjerps",
"cluster": "Runs on the FDE cluster operated by Data Forward Deployed Engineers Mohamed El Kadi and Bart Sjerps",
"admins": "Jody van Dongen and Laurens Rammers — ATC datacenter administrators",
"fde": "Mo and Bart — both Data Forward Deployed Engineers deploying AI workloads",
},
}
_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}
def format_fleet_memory_reply(result: dict) -> str:
"""Deterministic full table — LLMs truncate long fleet lists."""
servers = result.get("servers") or []
lines = [
"CURRENT installed RAM from OME inventory (not Dell catalog max).",
"known={k}/{n} · total_installed={tg} GB".format(
k=result.get("with_memory"),
n=result.get("count"),
tg=result.get("total_memory_gb"),
),
"",
"ST | name | model | memory_gb | dimms | idrac | connected",
]
for s in servers:
gb = s.get("memory_gb")
gb_s = "unknown" if gb is None else str(gb)
dc = s.get("dimm_count")
dc_s = "unknown" if dc is None else str(dc)
lines.append(
"{st} | {name} | {model} | {gb} | {dc} | {ip} | {conn}".format(
st=s.get("service_tag") or "NONE",
name=(s.get("name") or "")[:40],
model=(s.get("model") or "")[:28],
gb=gb_s,
dc=dc_s,
ip=s.get("idrac_ip") or "—",
conn="yes" if s.get("connected") else "no",
)
)
return "\n".join(lines)
def should_use_deterministic_fleet_memory(message: str, planned: list, tool_results: list[dict]) -> dict | None:
if not any(n == "list_fleet_memory" for n, _ in planned):
return None
# Prefer deterministic when the ask is clearly a full memory inventory
low = (message or "").lower()
inventoryish = bool(
re.search(r"\b(memory|ram|geheugen|dimm)\b", low)
and re.search(r"\b(elk|elke|iedere|alle|each|every|all|per\s+server|hoeveel|list|overview)\b", low)
)
# Or when list_fleet_memory is the only tool
only_mem = len(planned) == 1 and planned[0][0] == "list_fleet_memory"
if not (inventoryish or only_mem):
return None
for r in tool_results:
if r.get("tool") == "list_fleet_memory" and not r.get("error"):
return r
return None
@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
# 1) Plan + execute live tools BEFORE the LLM (accurate facts; Llama3-GPTQ lacks native tool_calls)
planned = plan_chat_tools(payload.message, payload.focus_device_id)
tool_results: list[dict] = []
if planned:
tool_results = list(
await asyncio.gather(*[run_chat_tool(name, args) for name, args in planned])
)
max_tokens = settings.vllm_max_tokens
if any(n == "list_fleet_memory" for n, _ in planned):
max_tokens = max(max_tokens, 1800)
# Full fleet memory lists: answer from TOOL FACTS directly (accurate, complete)
det = should_use_deterministic_fleet_memory(payload.message, planned, tool_results)
if det is not None:
gpu = STATE.get("gpu") or {}
gsum = gpu.get("summary") or {}
return {
"reply": format_fleet_memory_reply(det),
"model": model,
"backend": "ome-tools",
"gpu": gsum,
"context_bytes": 0,
"tools_used": [{"name": n, "args": a} for n, a in planned],
"tool_count": len(tool_results),
"deterministic": True,
}
system = build_fleet_context(payload.focus_device_id, max_chars=settings.chat_system_chars)
facts = format_tool_facts(tool_results)
if facts:
# Prefer tool facts; keep within budget (vLLM context is tight)
budget = max(1800, settings.chat_system_chars - 100)
# Accuracy rules + short fleet head, then authoritative TOOL FACTS
head = system
if len(system) > 2800:
head = system[:2800] + "\n…[fleet truncated for tools]"
combined = head + "\n\n" + facts
if len(combined) > budget:
# Keep TOOL FACTS intact; shrink head further
max_facts = min(len(facts), budget - 900)
facts_trim = facts[:max_facts]
head = system[: max(600, budget - len(facts_trim) - 40)]
combined = head + "\n\n\n" + facts_trim
system = combined[:budget]
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]})
user_msg = payload.message[:2500]
escalate = (
"If any needed fact is absent, say it is unknown and instruct the user to overleggen met "
"Jody van Dongen (jody.van.dongen@dell.com) and Laurens Rammers (laurens.rammers@dell.com). "
"Never invent."
)
if tool_results:
tool_errors = [r for r in tool_results if r.get("error")]
user_msg += (
"\n\n[System note: live TOOL FACTS were fetched for this question. "
"Treat TOOL FACTS as ground truth. Quote CURRENT vs CATALOG separately. "
"Do not hedge or invent. Say unknown only if a field is absent from TOOL FACTS. "
"For fleet lists: output EVERY row from TOOL FACTS in a compact table "
"(ST | name | model | memory_gb | dimms | idrac) — do not summarize or omit. "
f"{escalate}]"
)
if tool_errors:
user_msg += (
"\n[System note: some tools failed — do not fill gaps from model knowledge. "
f"{escalate}]"
)
else:
user_msg += (
"\n\n[System note: no live TOOL FACTS were fetched for this turn. "
"Answer only from the fleet snapshot in the system message. "
f"{escalate}]"
)
messages.append({"role": "user", "content": user_msg})
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.05,
"max_tokens": 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.05,
"max_tokens": 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=2800)
if facts:
tight = (tight + "\n\n" + facts)[:3800]
messages = [{"role": "system", "content": tight}, messages[-1]]
r = await client.post(
url,
json={
"model": model,
"messages": messages,
"temperature": 0.05,
"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),
"tools_used": [{"name": n, "args": a} for n, a in planned],
"tool_count": len(tool_results),
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(502, f"Chat backend error: {e}") from e
def _admin_name(aid: str) -> str:
u = _ops_user(aid)
return u["name"] if u else 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": OPS_USERS, "users": OPS_USERS}
@app.post("/api/tickets")
async def create_ticket(payload: TicketIn):
now = time.time()
assignee = payload.assignee
if not assignee or not _ops_user(assignee):
assignee = _default_assignee(payload.created_by)
if payload.created_by and not _ops_user(payload.created_by):
raise HTTPException(400, f"Unknown actor: {payload.created_by}")
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()
_backup_ops_db("create")
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": OPS_USERS, "users": OPS_USERS}
@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()
_backup_ops_db("message")
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()
_backup_ops_db("patch")
return dict(row)
@app.delete("/api/tickets/{ticket_id}")
async def delete_ticket(ticket_id: int):
_backup_ops_db("pre-delete")
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,))
_backup_ops_db("delete")
return {"ok": True, "deleted": ticket_id}
class RackIn(BaseModel):
site_id: str
name: str
units: int = 42
sort_order: int | None = None
class RackPatch(BaseModel):
name: str | None = None
sort_order: int | None = None
class PlacementIn(BaseModel):
rack_id: int
u_start: int
u_height: int | None = None
@app.get("/api/network/fabric")
async def api_network_fabric():
ensure_network_inventory_in_state()
return build_network_fabric()
class VlanIn(BaseModel):
vlan_id: int | None = None
name: str
cidr: str
purpose: str = ""
site_id: str | None = None
color: str = "#00a8e8"
sort_order: int = 0
@app.get("/api/network/vlans")
async def api_network_vlans():
return build_vlan_map()
@app.post("/api/network/vlans")
async def api_create_vlan(payload: VlanIn):
now = time.time()
try:
ipaddress.ip_network(payload.cidr, strict=False)
except Exception as e:
raise HTTPException(400, f"Invalid cidr: {e}") from e
with _db() as conn:
cur = conn.execute(
"""
INSERT INTO vlans(vlan_id, name, cidr, purpose, site_id, color, sort_order, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
payload.vlan_id,
payload.name.strip()[:80],
payload.cidr.strip(),
(payload.purpose or "")[:200],
payload.site_id,
payload.color or "#00a8e8",
payload.sort_order,
now,
now,
),
)
vid = cur.lastrowid
_backup_ops_db("vlan-create")
return {"id": vid, **build_vlan_map()}
class EndpointIn(BaseModel):
hostname: str | None = None
ip: str
role: str = "server"
kind: str = "host"
device_id: int | None = None
vlan_id: int | None = None
switch_id: int | None = None
switch_port: int | None = None
model: str | None = None
note: str | None = None
user_note: str | None = None
class EndpointPatch(BaseModel):
hostname: str | None = None
role: str | None = None
kind: str | None = None
device_id: int | None = None
vlan_id: int | None = None
switch_id: int | None = None
switch_port: int | None = None
model: str | None = None
note: str | None = None
user_note: str | None = None
clear_device_id: bool = False
@app.get("/api/network/endpoints")
async def api_list_endpoints():
ensure_network_inventory_in_state()
return {"endpoints": _load_network_endpoints()}
@app.post("/api/network/endpoints")
async def api_create_endpoint(payload: EndpointIn):
try:
ipaddress.ip_address(payload.ip.strip())
except Exception as e:
raise HTTPException(400, f"Invalid ip: {e}") from e
_upsert_network_endpoint(
hostname=(payload.hostname or "").strip() or None,
ip=payload.ip.strip(),
role=(payload.role or "server").strip()[:40],
kind=(payload.kind or "host").strip()[:40],
device_id=payload.device_id,
vlan_id=payload.vlan_id,
switch_id=payload.switch_id,
switch_port=payload.switch_port,
model=(payload.model or "").strip()[:80] or None,
note=(payload.note or "").strip()[:500] or None,
overwrite_identity=True,
)
if payload.user_note is not None:
with _db() as conn:
_ensure_network_endpoints_schema(conn)
conn.execute(
"UPDATE network_endpoints SET user_note=?, updated_at=? WHERE ip=?",
((payload.user_note or "")[:2000], time.time(), payload.ip.strip()),
)
ensure_network_inventory_in_state(force=True)
_backup_ops_db("endpoint-create")
row = next((e for e in _load_network_endpoints() if e.get("ip") == payload.ip.strip()), None)
return {"endpoint": row, "vlans": build_vlan_map()}
@app.patch("/api/network/endpoints/{endpoint_id}")
async def api_patch_endpoint(endpoint_id: int, payload: EndpointPatch):
with _db() as conn:
_ensure_network_endpoints_schema(conn)
row = conn.execute("SELECT * FROM network_endpoints WHERE id=?", (endpoint_id,)).fetchone()
if not row:
raise HTTPException(404, "Endpoint not found")
sets = ["updated_at=?"]
vals: list = [time.time()]
data = payload.model_dump(exclude_unset=True)
clear = bool(data.pop("clear_device_id", False))
mapping = {
"hostname": 80,
"role": 40,
"kind": 40,
"model": 80,
"note": 500,
"user_note": 2000,
}
for key, maxlen in mapping.items():
if key in data and data[key] is not None:
sets.append(f"{key}=?")
vals.append(str(data[key])[:maxlen])
for key in ("device_id", "vlan_id", "switch_id", "switch_port"):
if key in data and data[key] is not None:
sets.append(f"{key}=?")
vals.append(data[key])
if clear:
sets.append("device_id=NULL")
vals.append(endpoint_id)
conn.execute(f"UPDATE network_endpoints SET {', '.join(sets)} WHERE id=?", vals)
ensure_network_inventory_in_state(force=True)
_backup_ops_db("endpoint-patch")
row = next((e for e in _load_network_endpoints() if e.get("id") == endpoint_id), None)
return {"endpoint": row, "vlans": build_vlan_map()}
class PortLinkIn(BaseModel):
switch_port: int
device_id: int
device_port: str = ""
note: str = ""
@app.get("/api/network/switches/{switch_id}/ports")
async def api_switch_ports(switch_id: int):
return build_switch_portmap(switch_id)
@app.put("/api/network/switches/{switch_id}/ports/{port_num}")
async def api_put_port_link(switch_id: int, port_num: int, payload: PortLinkIn):
sw = next((d for d in (STATE.get("devices") or []) if d.get("id") == switch_id), None)
if not sw:
raise HTTPException(404, "Switch not found")
nports = _switch_port_count(sw)
if port_num < 1 or port_num > nports:
raise HTTPException(400, f"Port must be 1..{nports}")
peer = next((d for d in (STATE.get("devices") or []) if d.get("id") == payload.device_id), None)
if not peer:
raise HTTPException(404, "Target device not found in fleet")
now = time.time()
with _db() as conn:
conn.execute(
"""
INSERT INTO port_links(switch_id, switch_port, device_id, device_port, note, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(switch_id, switch_port) DO UPDATE SET
device_id=excluded.device_id,
device_port=excluded.device_port,
note=excluded.note,
updated_at=excluded.updated_at
""",
(
switch_id,
port_num,
payload.device_id,
(payload.device_port or "")[:80],
(payload.note or "")[:200],
now,
),
)
_backup_ops_db("port-link")
return build_switch_portmap(switch_id)
@app.delete("/api/network/switches/{switch_id}/ports/{port_num}")
async def api_delete_port_link(switch_id: int, port_num: int):
with _db() as conn:
conn.execute(
"DELETE FROM port_links WHERE switch_id=? AND switch_port=?",
(switch_id, port_num),
)
_backup_ops_db("port-unlink")
return build_switch_portmap(switch_id)
@app.get("/api/devices/{device_id}/nics")
async def api_device_nics(device_id: int):
"""OME serverNetworkInterfaces expanded to port/FQDD rows for wiring UI."""
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")
nics: list[dict] = []
try:
inv = await ome_fetch_inventory_types(device_id, ["serverNetworkInterfaces"])
for card in inv.get("serverNetworkInterfaces") or []:
nic_id = str(card.get("NicId") or "").strip()
vendor = str(card.get("VendorName") or card.get("Manufacturer") or "").strip()
ports = card.get("Ports") or []
if not ports:
# Rare flat inventory shape
fqdd = card.get("Fqdd") or card.get("InstanceId") or nic_id
name = (
card.get("ProductName")
or card.get("DeviceDescription")
or fqdd
or "NIC"
)
nics.append(
{
"nic_id": nic_id or None,
"port_id": fqdd,
"fqdd": fqdd,
"name": name,
"mac": card.get("PermanentMACAddress") or card.get("CurrentMACAddress"),
"speed": card.get("LinkSpeed") or card.get("Speed"),
"link": card.get("LinkStatus"),
"vendor": vendor or None,
"label": str(fqdd or name),
}
)
continue
for port in ports:
port_id = str(port.get("PortId") or "").strip()
parts = port.get("Partitions") or []
part = parts[0] if parts else {}
fqdd = str(part.get("Fqdd") or port_id or "").strip()
mac = (
part.get("PermanentMacAddress")
or part.get("CurrentMacAddress")
or part.get("PermanentMACAddress")
or part.get("CurrentMACAddress")
or ""
)
product = str(port.get("ProductName") or "").strip()
# OME often appends " - AA:BB:..." — keep product without MAC
if " - " in product and mac and product.upper().endswith(str(mac).upper()):
product = product[: product.rfind(" - ")].strip()
elif " - " in product:
left, right = product.rsplit(" - ", 1)
if ":" in right and len(right.replace(":", "")) >= 12:
product = left.strip()
link = port.get("LinkStatus")
speed = port.get("LinkSpeed")
label_bits = [port_id or fqdd or nic_id or "NIC"]
if product:
label_bits.append(product)
if mac:
label_bits.append(str(mac))
if link:
label_bits.append(str(link))
if speed not in (None, "", 0, "0"):
label_bits.append(f"{speed} Mb/s")
nics.append(
{
"nic_id": nic_id or None,
"port_id": port_id or None,
"fqdd": fqdd or port_id or None,
"name": product or port_id or nic_id or "NIC",
"mac": mac or None,
"speed": speed,
"link": link,
"vendor": vendor or None,
"label": " · ".join(label_bits),
}
)
except Exception as e:
log.debug("nic fetch %s: %s", device_id, e)
# Prefer unique port_id/fqdd order as returned by OME
return {"device_id": device_id, "nics": nics, "count": len(nics)}
@app.get("/api/racks")
async def api_racks():
_seed_atc_racks()
return _racks_payload()
@app.post("/api/racks")
async def api_create_rack(payload: RackIn):
now = time.time()
site = (payload.site_id or "").upper()
if site not in ("ATC1", "ATC2"):
raise HTTPException(400, "site_id must be ATC1 or ATC2")
with _db() as conn:
row = conn.execute("SELECT id FROM sites WHERE id=?", (site,)).fetchone()
if not row:
conn.execute(
"INSERT INTO sites(id, name, sort_order) VALUES (?, ?, ?)",
(site, site, 1 if site == "ATC1" else 2),
)
so = payload.sort_order
if so is None:
mx = conn.execute(
"SELECT COALESCE(MAX(sort_order), 0) AS m FROM racks WHERE site_id=?",
(site,),
).fetchone()["m"]
so = int(mx) + 1
cur = conn.execute(
"""
INSERT INTO racks(site_id, name, units, sort_order, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
""",
(site, payload.name.strip()[:64], max(1, min(48, payload.units)), so, now, now),
)
rid = cur.lastrowid
_backup_ops_db("rack-create")
return {"id": rid, **_racks_payload()}
@app.patch("/api/racks/{rack_id}")
async def api_patch_rack(rack_id: int, payload: RackPatch):
now = time.time()
with _db() as conn:
row = conn.execute("SELECT * FROM racks WHERE id=?", (rack_id,)).fetchone()
if not row:
raise HTTPException(404, "Rack not found")
name = payload.name.strip()[:64] if payload.name else row["name"]
so = payload.sort_order if payload.sort_order is not None else row["sort_order"]
conn.execute(
"UPDATE racks SET name=?, sort_order=?, updated_at=? WHERE id=?",
(name, so, now, rack_id),
)
_backup_ops_db("rack-patch")
return _racks_payload()
class RackItemIn(BaseModel):
rack_id: int
u_start: int = 1
u_height: int | None = None
side: str = "front" # front|rear|left|right
device_id: int | None = None
catalog_sku: str | None = None
label: str | None = None
notes: str | None = None
class RackItemPatch(BaseModel):
rack_id: int | None = None
u_start: int | None = None
u_height: int | None = None
side: str | None = None
catalog_sku: str | None = None
label: str | None = None
notes: str | None = None
@app.get("/api/rack-catalog")
async def api_rack_catalog():
families: dict[str, list] = {}
for item in DELL_RACK_CATALOG:
families.setdefault(item["family"], []).append(item)
return {"items": DELL_RACK_CATALOG, "families": families, "count": len(DELL_RACK_CATALOG)}
@app.put("/api/racks/placements/{device_id}")
async def api_put_placement(device_id: int, payload: PlacementIn):
"""Place/move an OME device (compat). Writes rack_items."""
device = next((d for d in (STATE.get("devices") or []) if d.get("id") == device_id), None)
cat = match_rack_catalog((device or {}).get("model"), (device or {}).get("role"))
body = RackItemIn(
rack_id=payload.rack_id,
u_start=payload.u_start,
u_height=payload.u_height if payload.u_height is not None else _default_u_height(device),
side="front",
device_id=device_id,
catalog_sku=(cat or {}).get("sku"),
label=(device or {}).get("name"),
)
return await api_upsert_rack_item(body)
@app.delete("/api/racks/placements/{device_id}")
async def api_delete_placement(device_id: int):
with _db() as conn:
conn.execute("DELETE FROM rack_items WHERE device_id=?", (device_id,))
try:
conn.execute("DELETE FROM rack_placements WHERE device_id=?", (device_id,))
except Exception:
pass
_backup_ops_db("unplace")
return _racks_payload()
@app.post("/api/racks/items")
async def api_upsert_rack_item(payload: RackItemIn):
now = time.time()
side = (payload.side or "front").lower()
if side not in ("front", "rear", "left", "right"):
raise HTTPException(400, "side must be front|rear|left|right")
device = None
if payload.device_id is not None:
device = next((d for d in (STATE.get("devices") or []) if d.get("id") == payload.device_id), None)
if not device:
raise HTTPException(404, "OME device not found")
sku = payload.catalog_sku
cat = next((c for c in DELL_RACK_CATALOG if c["sku"] == sku), None) if sku else None
if not cat and device:
cat = match_rack_catalog(device.get("model"), device.get("role"))
sku = (cat or {}).get("sku")
if not device and not cat and not sku:
raise HTTPException(400, "Provide device_id or catalog_sku")
height = payload.u_height
# Prefer catalog U-height for known SKUs so 2U/3U/… always place correctly
if cat is not None:
cat_h = int(cat.get("u_height") or 0)
if cat_h > 0:
height = cat_h
elif height is None:
height = _default_u_height(device)
elif height is None:
height = _default_u_height(device)
if side in ("left", "right"):
u_start, height = 1, 42
else:
height = max(1, min(42, int(height)))
u_start = int(payload.u_start)
if u_start < 1 or u_start + height - 1 > 42:
raise HTTPException(400, f"Must fit U1U42 (u_start={u_start} height={height})")
label = (payload.label or (device or {}).get("name") or (cat or {}).get("name") or "Device")[:120]
with _db() as conn:
rack = conn.execute("SELECT * FROM racks WHERE id=?", (payload.rack_id,)).fetchone()
if not rack:
raise HTTPException(404, "Rack not found")
if _rack_overlap(
conn,
payload.rack_id,
u_start if side not in ("left", "right") else 1,
height,
side=side,
exclude_device_id=payload.device_id,
):
raise HTTPException(409, "U range overlaps another item on this side")
existing = None
if payload.device_id is not None:
existing = conn.execute(
"SELECT id FROM rack_items WHERE device_id=?", (payload.device_id,)
).fetchone()
if existing:
conn.execute(
"""
UPDATE rack_items
SET rack_id=?, catalog_sku=?, label=?, u_start=?, u_height=?, side=?, notes=?, updated_at=?
WHERE id=?
""",
(
payload.rack_id,
sku,
label,
u_start if side not in ("left", "right") else 1,
height,
side,
(payload.notes or "")[:200],
now,
existing["id"],
),
)
item_id = existing["id"]
else:
cur = conn.execute(
"""
INSERT INTO rack_items(rack_id, device_id, catalog_sku, label, u_start, u_height, side, notes, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
payload.rack_id,
payload.device_id,
sku,
label,
u_start if side not in ("left", "right") else 1,
height,
side,
(payload.notes or "")[:200],
now,
),
)
item_id = cur.lastrowid
# keep legacy table in sync for OME devices
if payload.device_id is not None and side == "front":
try:
conn.execute(
"""
INSERT INTO rack_placements(device_id, rack_id, u_start, u_height, updated_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(device_id) DO UPDATE SET
rack_id=excluded.rack_id, u_start=excluded.u_start,
u_height=excluded.u_height, updated_at=excluded.updated_at
""",
(payload.device_id, payload.rack_id, u_start, height, now),
)
except Exception:
pass
_backup_ops_db("rack-item")
payload_out = _racks_payload()
payload_out["item_id"] = item_id
return payload_out
@app.patch("/api/racks/items/{item_id}")
async def api_patch_rack_item(item_id: int, payload: RackItemPatch):
now = time.time()
with _db() as conn:
row = conn.execute("SELECT * FROM rack_items WHERE id=?", (item_id,)).fetchone()
if not row:
raise HTTPException(404, "Item not found")
rack_id = payload.rack_id if payload.rack_id is not None else row["rack_id"]
side = (payload.side or row["side"] or "front").lower()
u_start = payload.u_start if payload.u_start is not None else row["u_start"]
height = payload.u_height if payload.u_height is not None else row["u_height"]
if side in ("left", "right"):
u_start, height = 1, 42
else:
height = max(1, min(42, int(height)))
u_start = int(u_start)
if u_start < 1 or u_start + height - 1 > 42:
raise HTTPException(400, "Must fit U1U42")
if _rack_overlap(conn, rack_id, u_start, height, side=side, exclude_item_id=item_id):
raise HTTPException(409, "U range overlaps another item on this side")
conn.execute(
"""
UPDATE rack_items
SET rack_id=?, catalog_sku=COALESCE(?, catalog_sku), label=COALESCE(?, label),
u_start=?, u_height=?, side=?, notes=COALESCE(?, notes), updated_at=?
WHERE id=?
""",
(
rack_id,
payload.catalog_sku,
payload.label,
u_start,
height,
side,
payload.notes,
now,
item_id,
),
)
_backup_ops_db("rack-item-patch")
return _racks_payload()
@app.delete("/api/racks/items/{item_id}")
async def api_delete_rack_item(item_id: int):
with _db() as conn:
row = conn.execute("SELECT * FROM rack_items WHERE id=?", (item_id,)).fetchone()
if not row:
raise HTTPException(404, "Item not found")
conn.execute("DELETE FROM rack_items WHERE id=?", (item_id,))
if row["device_id"] is not None:
try:
conn.execute("DELETE FROM rack_placements WHERE device_id=?", (row["device_id"],))
except Exception:
pass
_backup_ops_db("rack-item-delete")
return _racks_payload()
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"),
"idrac_ip": d.get("idrac_ip") or d.get("ip"),
"os_hostname": d.get("os_hostname"),
"rdp_host": d.get("rdp_host"),
"rdp_ips": d.get("rdp_ips") or [],
"os_ips": d.get("os_ips") or [],
"is_windows": d.get("is_windows"),
"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("/network.js")
async def network_js():
return FileResponse(STATIC_DIR / "network.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("/rdp.js")
async def rdp_js():
return FileResponse(STATIC_DIR / "rdp.js", media_type="application/javascript")
@app.get("/rdp-popout.html")
async def rdp_popout_html():
resp = FileResponse(STATIC_DIR / "rdp-popout.html")
resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
return resp
@app.get("/api/rdp.rdp")
async def api_rdp_file(host: str, port: int = 3389, username: str = ""):
"""Download a standard .rdp file for the local Remote Desktop client."""
host = (host or "").strip()
if not host or len(host) > 253 or any(c in host for c in "\r\n\x00"):
raise HTTPException(400, "Invalid host")
if port < 1 or port > 65535:
raise HTTPException(400, "Invalid port")
username = (username or "").strip()
if len(username) > 256 or any(c in username for c in "\r\n\x00"):
raise HTTPException(400, "Invalid username")
full = f"{host}:{port}" if port != 3389 else host
lines = [
"screen mode id:i:2",
"use multimon:i:0",
"desktopwidth:i:1920",
"desktopheight:i:1080",
"session bpp:i:32",
"compression:i:1",
"keyboardhook:i:2",
"audiocapturemode:i:0",
"videoplaybackmode:i:1",
"connection type:i:7",
"networkautodetect:i:1",
"bandwidthautodetect:i:1",
"displayconnectionbar:i:1",
"bitmapcachepersistenable:i:1",
f"full address:s:{full}",
"audiomode:i:0",
"redirectclipboard:i:1",
"autoreconnection enabled:i:1",
"authentication level:i:2",
"prompt for credentials:i:1",
"negotiate security layer:i:1",
"gatewayusagemethod:i:4",
"gatewaycredentialssource:i:4",
"gatewayprofileusagemethod:i:0",
]
if username:
lines.insert(15, f"username:s:{username}")
body = "\r\n".join(lines) + "\r\n"
safe = "".join(c if c.isalnum() or c in ".-_" else "_" for c in host)[:80] or "session"
return Response(
content=body,
media_type="application/x-rdp",
headers={
"Content-Disposition": f'attachment; filename="{safe}.rdp"',
"Cache-Control": "no-store",
},
)
@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("/present.js")
async def present_js():
return FileResponse(STATIC_DIR / "present.js", media_type="application/javascript")
@app.get("/logos/{filename}")
async def logo_asset(filename: str):
"""SVG / image logos for Present architecture slides."""
safe = Path(filename).name
if not safe or safe != filename or ".." in filename:
raise HTTPException(400, "invalid filename")
path = STATIC_DIR / "logos" / safe
if not path.is_file():
raise HTTPException(404, "logo not found")
media = "image/svg+xml" if safe.lower().endswith(".svg") else "image/png"
resp = FileResponse(path, media_type=media)
resp.headers["Cache-Control"] = "public, max-age=86400"
return resp
@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")
@app.get("/assets/dell/{filename}")
async def dell_asset(filename: str):
"""Serve Dell product face photos for rack Design Studio."""
safe = Path(filename).name
if not safe or safe != filename or ".." in filename:
raise HTTPException(400, "invalid filename")
path = STATIC_DIR / "assets" / "dell" / safe
if not path.is_file():
raise HTTPException(404, "asset not found")
media = "image/jpeg" if safe.lower().endswith((".jpg", ".jpeg")) else "image/png"
resp = FileResponse(path, media_type=media)
resp.headers["Cache-Control"] = "public, max-age=86400"
return resp