Add Reports drawer, Dell catalog compliance, and Service Tag chat context.
Exposes OME warranties, baselines, report defs, and CSV/JSON exports for customer-ready fleet briefs.
This commit is contained in:
+712
-9
@@ -35,10 +35,11 @@ class Settings(BaseSettings):
|
||||
vllm_model: str = "llama3-70b-gptq"
|
||||
vllm_max_model_len: int = 4096
|
||||
vllm_max_tokens: int = 700
|
||||
chat_system_chars: int = 5500
|
||||
chat_system_chars: int = 9000
|
||||
openwebui_email: str = ""
|
||||
openwebui_password: str = ""
|
||||
cockpit_data: str = "/data"
|
||||
reports_cache_ttl: float = 600.0
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
@@ -78,6 +79,77 @@ _lock = asyncio.Lock()
|
||||
DETAIL_CACHE: dict[int, dict] = {}
|
||||
PREV_DEVICES: dict[int, dict] = {}
|
||||
EVENT_FEED: list[dict] = []
|
||||
REPORT_CACHE: dict[str, Any] = {
|
||||
"warranties": None,
|
||||
"warranties_ts": 0.0,
|
||||
"compliance": None,
|
||||
"compliance_ts": 0.0,
|
||||
"baselines": None,
|
||||
"baselines_ts": 0.0,
|
||||
"catalogs": None,
|
||||
"catalogs_ts": 0.0,
|
||||
"report_defs": None,
|
||||
"report_defs_ts": 0.0,
|
||||
"jobs": None,
|
||||
"jobs_ts": 0.0,
|
||||
}
|
||||
|
||||
|
||||
async def ome_session(client: httpx.AsyncClient) -> tuple[str, dict, str]:
|
||||
"""Create an OME API session; returns (base, headers, session_id)."""
|
||||
base = settings.ome_url.rstrip("/")
|
||||
r = await client.post(
|
||||
f"{base}/api/SessionService/Sessions",
|
||||
json={
|
||||
"UserName": settings.ome_user,
|
||||
"Password": settings.ome_password,
|
||||
"SessionType": "API",
|
||||
},
|
||||
)
|
||||
r.raise_for_status()
|
||||
token = r.headers.get("X-Auth-Token")
|
||||
sid = str((r.json() or {}).get("Id") or "")
|
||||
headers = {"X-Auth-Token": token, "Accept": "application/json"}
|
||||
return base, headers, sid
|
||||
|
||||
|
||||
async def ome_session_delete(client: httpx.AsyncClient, base: str, headers: dict, sid: str) -> None:
|
||||
if not sid:
|
||||
return
|
||||
try:
|
||||
await client.delete(f"{base}/api/SessionService/Sessions('{sid}')", headers=headers)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _cache_get(key: str) -> Any | None:
|
||||
ts = float(REPORT_CACHE.get(f"{key}_ts") or 0)
|
||||
if REPORT_CACHE.get(key) is not None and time.time() - ts < settings.reports_cache_ttl:
|
||||
return REPORT_CACHE.get(key)
|
||||
return None
|
||||
|
||||
|
||||
def _cache_set(key: str, value: Any) -> Any:
|
||||
REPORT_CACHE[key] = value
|
||||
REPORT_CACHE[f"{key}_ts"] = time.time()
|
||||
return value
|
||||
|
||||
|
||||
def csv_escape(val: Any) -> str:
|
||||
s = "" if val is None else str(val)
|
||||
if any(c in s for c in (",", '"', "\n", "\r")):
|
||||
return '"' + s.replace('"', '""') + '"'
|
||||
return s
|
||||
|
||||
|
||||
def rows_to_csv(rows: list[dict], columns: list[str] | None = None) -> str:
|
||||
if not rows and not columns:
|
||||
return ""
|
||||
cols = columns or list(rows[0].keys())
|
||||
lines = [",".join(cols)]
|
||||
for row in rows:
|
||||
lines.append(",".join(csv_escape(row.get(c)) for c in cols))
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def subnet_of(ip: str | None) -> str:
|
||||
@@ -235,7 +307,9 @@ async def ome_fetch() -> dict:
|
||||
"id": d.get("Id"),
|
||||
"name": d.get("DeviceName") or f"device-{d.get('Id')}",
|
||||
"model": model,
|
||||
"service_tag": d.get("DeviceServiceTag"),
|
||||
"service_tag": d.get("DeviceServiceTag")
|
||||
or d.get("Identifier")
|
||||
or d.get("ChassisServiceTag"),
|
||||
"type": dtype,
|
||||
"sub_type": sub,
|
||||
"connected": conn,
|
||||
@@ -601,6 +675,22 @@ async def startup():
|
||||
init_db()
|
||||
asyncio.create_task(poll_loop())
|
||||
asyncio.create_task(gpu_loop())
|
||||
asyncio.create_task(warm_reports_cache())
|
||||
|
||||
|
||||
async def warm_reports_cache():
|
||||
"""Background warm of warranty/compliance for chat + inspector."""
|
||||
await asyncio.sleep(8)
|
||||
try:
|
||||
await fetch_warranties()
|
||||
await fetch_compliance()
|
||||
log.info(
|
||||
"Reports cache warmed: warranties=%s compliance_outdated=%s",
|
||||
(REPORT_CACHE.get("warranties") or {}).get("count"),
|
||||
((REPORT_CACHE.get("compliance") or {}).get("summary") or {}).get("outdated_devices"),
|
||||
)
|
||||
except Exception as e:
|
||||
log.warning("Reports cache warm failed: %s", e)
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
@@ -793,11 +883,32 @@ def build_fleet_context(focus_device_id: int | None = None, max_chars: int | Non
|
||||
critical = [a for a in alerts if a.get("severity") == "Critical"][:8]
|
||||
warnings = [a for a in alerts if a.get("severity") == "Warning"][:5]
|
||||
hottest = (ctx.get("hottest") or [])[:5]
|
||||
compliance = _cache_get("compliance") or REPORT_CACHE.get("compliance") or {}
|
||||
comp_sum = (compliance or {}).get("summary") or {}
|
||||
|
||||
# SERVICE TAG INDEX first — must survive truncation
|
||||
st_lines = [
|
||||
"SERVICE TAG INDEX (use these Service Tags in every node answer):",
|
||||
]
|
||||
for d in sorted(devices, key=lambda x: (x.get("name") or "").lower()):
|
||||
st = (d.get("service_tag") or "").strip() or "NONE"
|
||||
st_lines.append(
|
||||
"- ST={st} | {name} | ip={ip} | model={model} | connected={conn}".format(
|
||||
st=st,
|
||||
name=(d.get("name") or "")[:40],
|
||||
ip=d.get("ip") or "—",
|
||||
model=(d.get("model") or "")[:28],
|
||||
conn="yes" if d.get("connected") else "no",
|
||||
)
|
||||
)
|
||||
st_block = "\n".join(st_lines)
|
||||
|
||||
lines = [
|
||||
"You are OpenManage Cockpit Copilot for Dell ATC. Be concise and operational.",
|
||||
"Admins: Jody van Dongen, Laurens Rammers, Mohamed El Kadi.",
|
||||
"Use ONLY this live snapshot. Unknown => say unknown.",
|
||||
"ALWAYS include Service Tag (ST=...), model, and IP when discussing any system.",
|
||||
"If a Service Tag is missing in the index, say so explicitly.",
|
||||
"",
|
||||
"FLEET: total={t} connected={c} offline={o} watts={w} alerts_total={a}".format(
|
||||
t=summary.get("total"),
|
||||
@@ -806,11 +917,21 @@ def build_fleet_context(focus_device_id: int | None = None, max_chars: int | Non
|
||||
w=summary.get("total_watts"),
|
||||
a=summary.get("alerts_total"),
|
||||
),
|
||||
"CONNECTED:",
|
||||
]
|
||||
if comp_sum:
|
||||
lines.append(
|
||||
"FIRMWARE COMPLIANCE (Dell catalog baseline): outdated_devices={od} critical_components={cc} baseline={bn}".format(
|
||||
od=comp_sum.get("outdated_devices"),
|
||||
cc=comp_sum.get("critical_components"),
|
||||
bn=(comp_sum.get("baseline_name") or "")[:40],
|
||||
)
|
||||
)
|
||||
|
||||
lines.append("CONNECTED:")
|
||||
for d in connected[:18]:
|
||||
lines.append(
|
||||
"- {name} ip={ip} model={model} W={watts} st={status}".format(
|
||||
"- ST={st} {name} ip={ip} model={model} W={watts} status={status}".format(
|
||||
st=(d.get("service_tag") or "NONE"),
|
||||
name=(d.get("name") or "")[:36],
|
||||
ip=d.get("ip"),
|
||||
model=(d.get("model") or "")[:28],
|
||||
@@ -885,24 +1006,33 @@ def build_fleet_context(focus_device_id: int | None = None, max_chars: int | Non
|
||||
lines.append("FOCUS:")
|
||||
if node:
|
||||
lines.append(
|
||||
"{name} id={id} ip={ip} model={model} W={watts} connected={conn} status={st}".format(
|
||||
"ST={st} {name} id={id} ip={ip} model={model} W={watts} connected={conn} status={stt}".format(
|
||||
st=(node.get("service_tag") or "NONE"),
|
||||
name=node.get("name"),
|
||||
id=node.get("id"),
|
||||
ip=node.get("ip"),
|
||||
model=node.get("model"),
|
||||
watts=node.get("watts"),
|
||||
conn=node.get("connected"),
|
||||
st=node.get("status"),
|
||||
stt=node.get("status"),
|
||||
)
|
||||
)
|
||||
related = [a for a in alerts if a.get("device_id") == focus_device_id][:5]
|
||||
for a in related:
|
||||
lines.append("alert {sev}: {msg}".format(sev=a.get("severity"), msg=(a.get("message") or "")[:100]))
|
||||
lines.append(
|
||||
"alert {sev}: {msg}".format(sev=a.get("severity"), msg=(a.get("message") or "")[:100])
|
||||
)
|
||||
else:
|
||||
lines.append("device_id=%s missing" % focus_device_id)
|
||||
|
||||
lines.append("Reply with concrete names, IPs, and next actions.")
|
||||
out = "\n".join(lines)
|
||||
lines.append(
|
||||
"Reply with concrete Service Tags, names, IPs, models, and next actions."
|
||||
)
|
||||
body = "\n".join(lines)
|
||||
budget = max(800, limit - len(st_block) - 40)
|
||||
if len(body) > budget:
|
||||
body = body[: budget - 20] + "\n…[truncated]"
|
||||
out = st_block + "\n\n" + body
|
||||
if len(out) > limit:
|
||||
out = out[: limit - 20] + "\n…[truncated]"
|
||||
return out
|
||||
@@ -1489,6 +1619,574 @@ async def ws_ssh(ws: WebSocket):
|
||||
log.info("SSH session end id=%s target=%s active=%s", session_id, client_host, len(SSH_SESSIONS))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reports / compliance / warranty / export (read-only OME surfaces)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def fetch_warranties(force: bool = False) -> dict:
|
||||
if not force:
|
||||
cached = _cache_get("warranties")
|
||||
if cached is not None:
|
||||
return cached
|
||||
rows: list[dict] = []
|
||||
async with httpx.AsyncClient(verify=False, timeout=60.0) as client:
|
||||
base, headers, sid = await ome_session(client)
|
||||
try:
|
||||
skip = 0
|
||||
while True:
|
||||
r = await client.get(
|
||||
f"{base}/api/WarrantyService/Warranties",
|
||||
headers=headers,
|
||||
params={"$top": 200, "$skip": skip},
|
||||
)
|
||||
r.raise_for_status()
|
||||
chunk = (r.json() or {}).get("value") or []
|
||||
if not chunk:
|
||||
break
|
||||
for w in chunk:
|
||||
rows.append(
|
||||
{
|
||||
"id": w.get("Id"),
|
||||
"device_id": w.get("DeviceId"),
|
||||
"service_tag": w.get("DeviceIdentifier"),
|
||||
"device_name": w.get("DeviceName"),
|
||||
"model": w.get("DeviceModel"),
|
||||
"service_level": w.get("ServiceLevelDescription") or w.get("ServiceLevelCode"),
|
||||
"start_date": w.get("StartDate"),
|
||||
"end_date": w.get("EndDate"),
|
||||
"days_remaining": w.get("DaysRemaining"),
|
||||
"ship_date": w.get("SystemShipDate"),
|
||||
"state": w.get("State"),
|
||||
"country": w.get("CountryLookupCode"),
|
||||
}
|
||||
)
|
||||
skip += len(chunk)
|
||||
if len(chunk) < 200:
|
||||
break
|
||||
finally:
|
||||
await ome_session_delete(client, base, headers, sid)
|
||||
# Prefer longest-remaining warranty per device
|
||||
by_device: dict[int, dict] = {}
|
||||
for w in rows:
|
||||
did = w.get("device_id")
|
||||
if did is None:
|
||||
continue
|
||||
prev = by_device.get(did)
|
||||
if not prev or int(w.get("days_remaining") or 0) > int(prev.get("days_remaining") or 0):
|
||||
by_device[did] = w
|
||||
elif int(w.get("days_remaining") or 0) == int(prev.get("days_remaining") or 0):
|
||||
# keep later end date
|
||||
if str(w.get("end_date") or "") > str(prev.get("end_date") or ""):
|
||||
by_device[did] = w
|
||||
payload = {
|
||||
"updated_at": time.time(),
|
||||
"count": len(rows),
|
||||
"items": rows,
|
||||
"by_device": {str(k): v for k, v in by_device.items()},
|
||||
}
|
||||
return _cache_set("warranties", payload)
|
||||
|
||||
|
||||
async def fetch_baselines(force: bool = False) -> dict:
|
||||
if not force:
|
||||
cached = _cache_get("baselines")
|
||||
if cached is not None:
|
||||
return cached
|
||||
async with httpx.AsyncClient(verify=False, timeout=60.0) as client:
|
||||
base, headers, sid = await ome_session(client)
|
||||
try:
|
||||
r = await client.get(f"{base}/api/UpdateService/Baselines", headers=headers)
|
||||
r.raise_for_status()
|
||||
items = []
|
||||
for b in (r.json() or {}).get("value") or []:
|
||||
summary = b.get("ComplianceSummary") or {}
|
||||
items.append(
|
||||
{
|
||||
"id": b.get("Id"),
|
||||
"name": b.get("Name"),
|
||||
"description": b.get("Description"),
|
||||
"catalog_id": b.get("CatalogId"),
|
||||
"catalog_type": b.get("CatalogType"),
|
||||
"last_run": b.get("LastRun"),
|
||||
"task_id": b.get("TaskId"),
|
||||
"compliance_status": summary.get("ComplianceStatus"),
|
||||
"critical": summary.get("NumberOfCritical"),
|
||||
"warning": summary.get("NumberOfWarning"),
|
||||
"normal": summary.get("NumberOfNormal"),
|
||||
"downgrade": summary.get("NumberOfDowngrade"),
|
||||
"unknown": summary.get("NumberOfUnknown"),
|
||||
}
|
||||
)
|
||||
finally:
|
||||
await ome_session_delete(client, base, headers, sid)
|
||||
return _cache_set("baselines", {"updated_at": time.time(), "items": items})
|
||||
|
||||
|
||||
async def fetch_catalogs(force: bool = False) -> dict:
|
||||
if not force:
|
||||
cached = _cache_get("catalogs")
|
||||
if cached is not None:
|
||||
return cached
|
||||
async with httpx.AsyncClient(verify=False, timeout=60.0) as client:
|
||||
base, headers, sid = await ome_session(client)
|
||||
try:
|
||||
r = await client.get(f"{base}/api/UpdateService/Catalogs", headers=headers)
|
||||
r.raise_for_status()
|
||||
items = []
|
||||
for c in (r.json() or {}).get("value") or []:
|
||||
repo = c.get("Repository") or {}
|
||||
items.append(
|
||||
{
|
||||
"id": c.get("Id"),
|
||||
"filename": c.get("Filename") or c.get("SourcePath"),
|
||||
"source": repo.get("Source") or c.get("Source"),
|
||||
"repository_name": repo.get("Name"),
|
||||
"repository_type": repo.get("RepositoryType") or c.get("RepositoryType"),
|
||||
"owner": c.get("Owner"),
|
||||
"status": (c.get("Status") or {}).get("Name") if isinstance(c.get("Status"), dict) else c.get("Status"),
|
||||
"last_updated": c.get("LastUpdated") or c.get("BundlesUpdateTime"),
|
||||
}
|
||||
)
|
||||
finally:
|
||||
await ome_session_delete(client, base, headers, sid)
|
||||
return _cache_set("catalogs", {"updated_at": time.time(), "items": items})
|
||||
|
||||
|
||||
def _pick_primary_baseline(baselines: list[dict]) -> dict | None:
|
||||
if not baselines:
|
||||
return None
|
||||
for b in baselines:
|
||||
name = (b.get("name") or "").lower()
|
||||
if "dell" in name and "online" in name:
|
||||
return b
|
||||
# most critical first
|
||||
return sorted(baselines, key=lambda x: int(x.get("critical") or 0), reverse=True)[0]
|
||||
|
||||
|
||||
async def fetch_compliance(force: bool = False, baseline_id: int | None = None) -> dict:
|
||||
if not force and baseline_id is None:
|
||||
cached = _cache_get("compliance")
|
||||
if cached is not None:
|
||||
return cached
|
||||
bl = await fetch_baselines(force=force)
|
||||
primary = None
|
||||
if baseline_id is not None:
|
||||
primary = next((b for b in bl["items"] if b.get("id") == baseline_id), None)
|
||||
if primary is None:
|
||||
primary = _pick_primary_baseline(bl["items"])
|
||||
if not primary:
|
||||
payload = {
|
||||
"updated_at": time.time(),
|
||||
"summary": {"outdated_devices": 0, "critical_components": 0, "baseline_name": None},
|
||||
"devices": [],
|
||||
"components": [],
|
||||
"baselines": bl["items"],
|
||||
}
|
||||
return _cache_set("compliance", payload)
|
||||
|
||||
bid = primary["id"]
|
||||
devices_out: list[dict] = []
|
||||
components_out: list[dict] = []
|
||||
fleet_by_id = {d.get("id"): d for d in STATE.get("devices") or []}
|
||||
async with httpx.AsyncClient(verify=False, timeout=120.0) as client:
|
||||
base, headers, sid = await ome_session(client)
|
||||
try:
|
||||
r = await client.get(
|
||||
f"{base}/api/UpdateService/Baselines({bid})/DeviceComplianceReports",
|
||||
headers=headers,
|
||||
)
|
||||
r.raise_for_status()
|
||||
for dcr in (r.json() or {}).get("value") or []:
|
||||
did = dcr.get("DeviceId")
|
||||
fleet = fleet_by_id.get(did) or {}
|
||||
st = dcr.get("ServiceTag") or fleet.get("service_tag")
|
||||
comps = dcr.get("ComponentComplianceReports") or []
|
||||
device_row = {
|
||||
"device_id": did,
|
||||
"service_tag": st,
|
||||
"name": fleet.get("name") or dcr.get("DeviceName") or st,
|
||||
"model": dcr.get("DeviceModel") or fleet.get("model"),
|
||||
"ip": fleet.get("ip"),
|
||||
"firmware_status": dcr.get("FirmwareStatus"),
|
||||
"compliance_status": dcr.get("ComplianceStatus"),
|
||||
"reboot_required": dcr.get("RebootRequired"),
|
||||
"component_count": len(comps),
|
||||
"dell_uri": None,
|
||||
}
|
||||
devices_out.append(device_row)
|
||||
for comp in comps:
|
||||
uri = comp.get("Uri")
|
||||
if uri and not device_row["dell_uri"]:
|
||||
device_row["dell_uri"] = uri
|
||||
components_out.append(
|
||||
{
|
||||
"device_id": did,
|
||||
"service_tag": st,
|
||||
"device_name": device_row["name"],
|
||||
"model": device_row["model"],
|
||||
"ip": device_row["ip"],
|
||||
"component": comp.get("Name"),
|
||||
"component_type": comp.get("ComponentType"),
|
||||
"current_version": comp.get("CurrentVersion"),
|
||||
"catalog_version": comp.get("Version"),
|
||||
"update_action": comp.get("UpdateAction"),
|
||||
"criticality": comp.get("Criticality"),
|
||||
"compliance_status": comp.get("ComplianceStatus"),
|
||||
"reboot_required": comp.get("RebootRequired"),
|
||||
"dell_uri": uri,
|
||||
"path": comp.get("Path"),
|
||||
"baseline_id": bid,
|
||||
"baseline_name": primary.get("name"),
|
||||
"status_badge": (
|
||||
"outdated"
|
||||
if str(comp.get("UpdateAction") or "").upper() == "UPGRADE"
|
||||
or str(comp.get("ComplianceStatus") or "").upper()
|
||||
in ("CRITICAL", "WARNING", "NONCOMPLIANT", "NON-COMPLIANT")
|
||||
else "current"
|
||||
if str(comp.get("UpdateAction") or "").upper() in ("EQUAL", "NONE", "")
|
||||
and str(comp.get("ComplianceStatus") or "").upper()
|
||||
in ("", "OK", "COMPLIANT", "NORMAL", "DOWNGRADE")
|
||||
else "unknown"
|
||||
),
|
||||
}
|
||||
)
|
||||
finally:
|
||||
await ome_session_delete(client, base, headers, sid)
|
||||
|
||||
outdated = [
|
||||
d
|
||||
for d in devices_out
|
||||
if str(d.get("compliance_status") or "").upper() in ("CRITICAL", "WARNING")
|
||||
or str(d.get("firmware_status") or "").lower() in ("non-compliant", "noncompliant")
|
||||
]
|
||||
crit_comps = [
|
||||
c
|
||||
for c in components_out
|
||||
if str(c.get("compliance_status") or "").upper() == "CRITICAL"
|
||||
or str(c.get("update_action") or "").upper() == "UPGRADE"
|
||||
]
|
||||
payload = {
|
||||
"updated_at": time.time(),
|
||||
"baseline": primary,
|
||||
"baselines": bl["items"],
|
||||
"summary": {
|
||||
"baseline_id": bid,
|
||||
"baseline_name": primary.get("name"),
|
||||
"outdated_devices": len(outdated),
|
||||
"devices_in_report": len(devices_out),
|
||||
"critical_components": len(crit_comps),
|
||||
"components_total": len(components_out),
|
||||
"compliance_status": primary.get("compliance_status"),
|
||||
"last_run": primary.get("last_run"),
|
||||
},
|
||||
"devices": devices_out,
|
||||
"components": components_out,
|
||||
}
|
||||
if baseline_id is None:
|
||||
return _cache_set("compliance", payload)
|
||||
return payload
|
||||
|
||||
|
||||
async def fetch_report_defs(force: bool = False) -> dict:
|
||||
if not force:
|
||||
cached = _cache_get("report_defs")
|
||||
if cached is not None:
|
||||
return cached
|
||||
items = []
|
||||
async with httpx.AsyncClient(verify=False, timeout=60.0) as client:
|
||||
base, headers, sid = await ome_session(client)
|
||||
try:
|
||||
r = await client.get(f"{base}/api/ReportService/ReportDefs", headers=headers)
|
||||
r.raise_for_status()
|
||||
for d in (r.json() or {}).get("value") or []:
|
||||
cols = [c.get("Name") for c in (d.get("ColumnNames") or []) if c.get("Name")]
|
||||
items.append(
|
||||
{
|
||||
"id": d.get("Id"),
|
||||
"name": d.get("Name"),
|
||||
"description": d.get("Description"),
|
||||
"category": d.get("Category") or d.get("FilterGroupName"),
|
||||
"is_builtin": d.get("IsBuiltIn"),
|
||||
"last_run": d.get("LastRunDate"),
|
||||
"last_run_by": d.get("LastRunBy"),
|
||||
"columns": cols,
|
||||
}
|
||||
)
|
||||
finally:
|
||||
await ome_session_delete(client, base, headers, sid)
|
||||
items.sort(key=lambda x: ((x.get("category") or ""), (x.get("name") or "").lower()))
|
||||
return _cache_set("report_defs", {"updated_at": time.time(), "items": items})
|
||||
|
||||
|
||||
async def fetch_jobs(force: bool = False, top: int = 40) -> dict:
|
||||
if not force:
|
||||
cached = _cache_get("jobs")
|
||||
if cached is not None:
|
||||
return cached
|
||||
items = []
|
||||
async with httpx.AsyncClient(verify=False, timeout=60.0) as client:
|
||||
base, headers, sid = await ome_session(client)
|
||||
try:
|
||||
r = await client.get(
|
||||
f"{base}/api/JobService/Jobs",
|
||||
headers=headers,
|
||||
params={"$top": top},
|
||||
)
|
||||
r.raise_for_status()
|
||||
for j in (r.json() or {}).get("value") or []:
|
||||
status = j.get("LastRunStatus") or {}
|
||||
items.append(
|
||||
{
|
||||
"id": j.get("Id"),
|
||||
"name": j.get("JobName") or j.get("Name"),
|
||||
"status": status.get("Name") if isinstance(status, dict) else status,
|
||||
"job_type": (j.get("JobType") or {}).get("Name")
|
||||
if isinstance(j.get("JobType"), dict)
|
||||
else j.get("JobType"),
|
||||
"last_run": j.get("LastRunStatus") and (j.get("LastRunDate") or j.get("StartTime")),
|
||||
"progress": j.get("Progress") or j.get("PercentComplete"),
|
||||
"created_by": j.get("CreatedBy"),
|
||||
}
|
||||
)
|
||||
finally:
|
||||
await ome_session_delete(client, base, headers, sid)
|
||||
return _cache_set("jobs", {"updated_at": time.time(), "items": items})
|
||||
|
||||
|
||||
def build_fleet_report_rows(warranties: dict | None = None) -> list[dict]:
|
||||
by_dev = (warranties or {}).get("by_device") or {}
|
||||
rows = []
|
||||
for d in STATE.get("devices") or []:
|
||||
w = by_dev.get(str(d.get("id"))) or {}
|
||||
rows.append(
|
||||
{
|
||||
"id": d.get("id"),
|
||||
"name": d.get("name"),
|
||||
"service_tag": d.get("service_tag"),
|
||||
"model": d.get("model"),
|
||||
"ip": d.get("ip"),
|
||||
"subnet": d.get("subnet"),
|
||||
"type": d.get("type"),
|
||||
"sub_type": d.get("sub_type"),
|
||||
"is_server": d.get("is_server"),
|
||||
"is_idrac": d.get("is_idrac"),
|
||||
"connected": d.get("connected"),
|
||||
"powered_on": d.get("powered_on"),
|
||||
"power_state": d.get("power_state"),
|
||||
"status": d.get("status"),
|
||||
"watts": d.get("watts"),
|
||||
"avg_watts": d.get("avg_watts"),
|
||||
"peak_watts": d.get("peak_watts"),
|
||||
"last_status_time": d.get("last_status_time"),
|
||||
"last_inventory_time": d.get("last_inventory_time"),
|
||||
"warranty_end": w.get("end_date"),
|
||||
"warranty_days_remaining": w.get("days_remaining"),
|
||||
"warranty_service_level": w.get("service_level"),
|
||||
}
|
||||
)
|
||||
rows.sort(key=lambda r: (r.get("name") or "").lower())
|
||||
return rows
|
||||
|
||||
|
||||
@app.get("/api/warranties")
|
||||
async def api_warranties(force: bool = False):
|
||||
return await fetch_warranties(force=force)
|
||||
|
||||
|
||||
@app.get("/api/compliance")
|
||||
async def api_compliance(force: bool = False, baseline_id: int | None = None):
|
||||
return await fetch_compliance(force=force, baseline_id=baseline_id)
|
||||
|
||||
|
||||
@app.get("/api/baselines")
|
||||
async def api_baselines(force: bool = False):
|
||||
return await fetch_baselines(force=force)
|
||||
|
||||
|
||||
@app.get("/api/catalogs")
|
||||
async def api_catalogs(force: bool = False):
|
||||
return await fetch_catalogs(force=force)
|
||||
|
||||
|
||||
@app.get("/api/ome/report-defs")
|
||||
async def api_report_defs(force: bool = False):
|
||||
return await fetch_report_defs(force=force)
|
||||
|
||||
|
||||
@app.get("/api/ome/jobs")
|
||||
async def api_ome_jobs(force: bool = False):
|
||||
return await fetch_jobs(force=force)
|
||||
|
||||
|
||||
class ReportRunIn(BaseModel):
|
||||
report_def_id: int
|
||||
|
||||
|
||||
@app.post("/api/ome/reports/run")
|
||||
async def api_ome_report_run(payload: ReportRunIn):
|
||||
async with httpx.AsyncClient(verify=False, timeout=60.0) as client:
|
||||
base, headers, sid = await ome_session(client)
|
||||
try:
|
||||
r = await client.post(
|
||||
f"{base}/api/ReportService/Actions/ReportService.RunReport",
|
||||
headers=headers,
|
||||
json={"ReportDefId": payload.report_def_id},
|
||||
)
|
||||
if r.status_code >= 400:
|
||||
raise HTTPException(r.status_code, r.text[:500])
|
||||
job_id = r.json() if isinstance(r.json(), (int, str)) else (r.json() or {}).get("Id") or r.text
|
||||
return {
|
||||
"job_id": job_id,
|
||||
"report_def_id": payload.report_def_id,
|
||||
"message": "Report job started in OME. Poll results shortly.",
|
||||
"results_url": f"/api/ome/reports/{payload.report_def_id}/results",
|
||||
}
|
||||
finally:
|
||||
await ome_session_delete(client, base, headers, sid)
|
||||
|
||||
|
||||
@app.get("/api/ome/reports/{report_def_id}/results")
|
||||
async def api_ome_report_results(report_def_id: int):
|
||||
async with httpx.AsyncClient(verify=False, timeout=60.0) as client:
|
||||
base, headers, sid = await ome_session(client)
|
||||
try:
|
||||
r = await client.get(
|
||||
f"{base}/api/ReportService/ReportDefs({report_def_id})/ReportResults",
|
||||
headers=headers,
|
||||
)
|
||||
if r.status_code >= 400:
|
||||
raise HTTPException(
|
||||
r.status_code,
|
||||
(r.json().get("error", {}) or {}).get("message")
|
||||
if r.headers.get("content-type", "").startswith("application/json")
|
||||
else r.text[:500],
|
||||
)
|
||||
return r.json()
|
||||
finally:
|
||||
await ome_session_delete(client, base, headers, sid)
|
||||
|
||||
|
||||
@app.get("/api/reports/fleet")
|
||||
async def api_reports_fleet(force: bool = False):
|
||||
warranties = await fetch_warranties(force=force)
|
||||
rows = build_fleet_report_rows(warranties)
|
||||
return {
|
||||
"updated_at": STATE.get("updated_at"),
|
||||
"summary": STATE.get("summary"),
|
||||
"ome": STATE.get("ome"),
|
||||
"count": len(rows),
|
||||
"rows": rows,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/reports/firmware")
|
||||
async def api_reports_firmware(force: bool = False, baseline_id: int | None = None):
|
||||
return await fetch_compliance(force=force, baseline_id=baseline_id)
|
||||
|
||||
|
||||
@app.get("/api/reports/brief")
|
||||
async def api_reports_brief(force: bool = False):
|
||||
warranties = await fetch_warranties(force=force)
|
||||
compliance = await fetch_compliance(force=force)
|
||||
fleet_rows = build_fleet_report_rows(warranties)
|
||||
critical = [a for a in (STATE.get("alerts") or []) if a.get("severity") == "Critical"][:15]
|
||||
expiring = sorted(
|
||||
[
|
||||
w
|
||||
for w in (warranties.get("items") or [])
|
||||
if w.get("days_remaining") is not None and int(w.get("days_remaining") or 0) <= 90
|
||||
],
|
||||
key=lambda x: int(x.get("days_remaining") or 0),
|
||||
)[:25]
|
||||
outdated = [
|
||||
d
|
||||
for d in (compliance.get("devices") or [])
|
||||
if str(d.get("compliance_status") or "").upper() in ("CRITICAL", "WARNING")
|
||||
or str(d.get("firmware_status") or "").lower() in ("non-compliant", "noncompliant")
|
||||
]
|
||||
return {
|
||||
"generated_at": time.time(),
|
||||
"ome": STATE.get("ome"),
|
||||
"summary": STATE.get("summary"),
|
||||
"compliance_summary": compliance.get("summary"),
|
||||
"critical_alerts": critical,
|
||||
"outdated_devices": outdated[:40],
|
||||
"warranty_expiring": expiring,
|
||||
"fleet": fleet_rows,
|
||||
"baseline": compliance.get("baseline"),
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/export/{kind}")
|
||||
async def api_export(kind: str, fmt: str = "csv", force: bool = False, baseline_id: int | None = None):
|
||||
kind = kind.lower().strip()
|
||||
fmt = fmt.lower().strip()
|
||||
if fmt not in ("csv", "json"):
|
||||
raise HTTPException(400, "fmt must be csv or json")
|
||||
|
||||
if kind in ("fleet", "inventory"):
|
||||
data = await api_reports_fleet(force=force)
|
||||
rows = data["rows"]
|
||||
filename = f"ome-fleet-{int(time.time())}"
|
||||
elif kind in ("firmware", "compliance"):
|
||||
data = await fetch_compliance(force=force, baseline_id=baseline_id)
|
||||
rows = data.get("components") or []
|
||||
filename = f"ome-firmware-compliance-{int(time.time())}"
|
||||
elif kind == "warranty":
|
||||
data = await fetch_warranties(force=force)
|
||||
rows = data.get("items") or []
|
||||
filename = f"ome-warranties-{int(time.time())}"
|
||||
elif kind == "brief":
|
||||
data = await api_reports_brief(force=force)
|
||||
if fmt == "json":
|
||||
return Response(
|
||||
content=json.dumps(data, indent=2, default=str),
|
||||
media_type="application/json",
|
||||
headers={"Content-Disposition": f'attachment; filename="ome-customer-brief-{int(time.time())}.json"'},
|
||||
)
|
||||
# flatten brief as fleet CSV appendix
|
||||
rows = data.get("fleet") or []
|
||||
filename = f"ome-customer-brief-fleet-{int(time.time())}"
|
||||
else:
|
||||
raise HTTPException(404, "Unknown export kind. Use fleet|firmware|warranty|brief")
|
||||
|
||||
if fmt == "json":
|
||||
body = json.dumps({"kind": kind, "count": len(rows), "rows": rows}, indent=2, default=str)
|
||||
return Response(
|
||||
content=body,
|
||||
media_type="application/json",
|
||||
headers={"Content-Disposition": f'attachment; filename="{filename}.json"'},
|
||||
)
|
||||
csv_body = rows_to_csv(rows)
|
||||
return Response(
|
||||
content=csv_body,
|
||||
media_type="text/csv",
|
||||
headers={"Content-Disposition": f'attachment; filename="{filename}.csv"'},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/devices/{device_id}/warranty")
|
||||
async def device_warranty(device_id: int, force: bool = False):
|
||||
warranties = await fetch_warranties(force=force)
|
||||
items = [w for w in warranties.get("items") or [] if w.get("device_id") == device_id]
|
||||
primary = (warranties.get("by_device") or {}).get(str(device_id))
|
||||
return {"device_id": device_id, "primary": primary, "items": items}
|
||||
|
||||
|
||||
@app.get("/api/devices/{device_id}/compliance")
|
||||
async def device_compliance(device_id: int, force: bool = False):
|
||||
compliance = await fetch_compliance(force=force)
|
||||
device = next((d for d in compliance.get("devices") or [] if d.get("device_id") == device_id), None)
|
||||
comps = [c for c in compliance.get("components") or [] if c.get("device_id") == device_id]
|
||||
return {
|
||||
"device_id": device_id,
|
||||
"baseline": compliance.get("baseline"),
|
||||
"device": device,
|
||||
"components": comps,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/ssh/sessions")
|
||||
async def api_ssh_sessions():
|
||||
"""Ops visibility: active SSH bridges (no secrets)."""
|
||||
@@ -1561,6 +2259,11 @@ async def vendor_xterm_fit_js():
|
||||
)
|
||||
|
||||
|
||||
@app.get("/reports.js")
|
||||
async def reports_js():
|
||||
return FileResponse(STATIC_DIR / "reports.js", media_type="application/javascript")
|
||||
|
||||
|
||||
@app.get("/dell.png")
|
||||
async def dell_png():
|
||||
return FileResponse(STATIC_DIR / "dell.png", media_type="image/png")
|
||||
|
||||
Reference in New Issue
Block a user