first commit

This commit is contained in:
mega 2026-03-19 11:32:17 +00:00
commit 4b98219bf7
144 changed files with 31561 additions and 0 deletions

View file

@ -0,0 +1,344 @@
import hashlib
from fastapi import APIRouter, Depends, Query
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from core.database import get_session
router = APIRouter()
# Mirrors the simulator topology — single source of truth for site layout
TOPOLOGY = {
"sg-01": {
"rooms": [
{"room_id": "hall-a", "racks": [f"SG1A01.{i:02d}" for i in range(1, 21)] + [f"SG1A02.{i:02d}" for i in range(1, 21)], "crac_id": "crac-01"},
{"room_id": "hall-b", "racks": [f"SG1B01.{i:02d}" for i in range(1, 21)] + [f"SG1B02.{i:02d}" for i in range(1, 21)], "crac_id": "crac-02"},
],
"ups_units": ["ups-01", "ups-02"],
"leak_sensors": ["leak-01"],
}
}
# ── Device catalog ────────────────────────────────────────────────────────────
# Each tuple: (name, u_height, power_draw_w)
_SERVERS = [
("Dell PowerEdge R750", 2, 420),
("HPE ProLiant DL380 Gen10 Plus", 2, 380),
("Supermicro SuperServer 2029P", 2, 350),
("Dell PowerEdge R650xs", 1, 280),
("HPE ProLiant DL360 Gen10 Plus", 1, 260),
]
_SWITCHES = [
("Cisco Catalyst C9300-48P", 1, 60),
("Arista 7050CX3-32S", 1, 180),
("Juniper EX4300-48T", 1, 75),
]
_PATCHES = [
("Leviton 24-Port Cat6A Patch Panel", 1, 5),
("Panduit 48-Port Cat6A Patch Panel", 1, 5),
]
_PDUS = [
("APC AP8888 Metered Rack PDU", 1, 10),
("Raritan PX3-5190R Metered PDU", 1, 10),
]
_STORAGE = [
("Dell EMC PowerVault ME5024", 2, 280),
("NetApp AFF C190", 2, 200),
]
_FIREWALL = [
("Palo Alto PA-5220", 2, 150),
("Fortinet FortiGate 3000F",2, 180),
]
_KVM = [("Raritan KX III-464", 1, 15)]
def _serial(rack_id: str, u: int) -> str:
return hashlib.md5(f"{rack_id}-u{u}".encode()).hexdigest()[:10].upper()
def _rack_seq(rack_id: str) -> int:
"""SG1A01.05 → 5, SG1A02.05 → 25, SG1B01.05 → 5"""
# Format: SG1A01.05 — row at [4:6], rack num after dot
row = int(rack_id[4:6]) # "01" or "02"
num = int(rack_id[7:]) # "01" to "20"
return (row - 1) * 20 + num
def _generate_devices(site_id: str, room_id: str, rack_id: str) -> list[dict]:
s = _rack_seq(rack_id)
room_oct = "1" if room_id == "hall-a" else "2"
devices: list[dict] = []
u = 1
def add(name: str, dtype: str, u_start: int, u_height: int, power_w: int, ip: str = "-"):
devices.append({
"device_id": f"{rack_id}-u{u_start:02d}",
"name": name,
"type": dtype,
"rack_id": rack_id,
"room_id": room_id,
"site_id": site_id,
"u_start": u_start,
"u_height": u_height,
"ip": ip,
"serial": _serial(rack_id, u_start),
"model": name,
"status": "online",
"power_draw_w": power_w,
})
# U1: Patch panel
p = _PATCHES[s % len(_PATCHES)]
add(p[0], "patch_panel", u, p[1], p[2]); u += p[1]
# U2: Switch
sw = _SWITCHES[s % len(_SWITCHES)]
add(sw[0], "switch", u, sw[1], sw[2], f"10.10.{room_oct}.{s}"); u += sw[1]
# KVM in rack 5 / 15
if s in (5, 15):
kvm = _KVM[0]
add(kvm[0], "kvm", u, kvm[1], kvm[2], f"10.10.{room_oct}.{s + 100}"); u += kvm[1]
# Firewall in first rack of each room
if rack_id in ("SG1A01.01", "SG1B01.01"):
fw = _FIREWALL[s % len(_FIREWALL)]
add(fw[0], "firewall", u, fw[1], fw[2], f"10.10.{room_oct}.254"); u += fw[1]
# Storage in rack 3 and 13
if s in (3, 13):
stor = _STORAGE[s % len(_STORAGE)]
add(stor[0], "storage", u, stor[1], stor[2], f"10.10.{room_oct}.{s + 50}"); u += stor[1]
# Servers filling U slots up to U41
srv_pool = (_SERVERS * 3)
ip_counter = (s - 1) * 15 + 10
for idx, (name, u_h, pwr) in enumerate(srv_pool):
if u + u_h > 41:
break
# Occasional empty gap for realism
if idx > 0 and (s + idx) % 8 == 0 and u + u_h + 1 <= 41:
u += 1
if u + u_h > 41:
break
add(name, "server", u, u_h, pwr, f"10.10.{room_oct}.{ip_counter}"); u += u_h
ip_counter += 1
# U42: PDU
pdu = _PDUS[s % len(_PDUS)]
add(pdu[0], "pdu", 42, pdu[1], pdu[2])
return devices
# ── Endpoints ─────────────────────────────────────────────────────────────────
@router.get("")
async def get_assets(
site_id: str = Query(...),
session: AsyncSession = Depends(get_session),
):
site = TOPOLOGY.get(site_id)
if not site:
return {"site_id": site_id, "rooms": [], "ups_units": []}
result = await session.execute(text("""
SELECT DISTINCT ON (sensor_id)
sensor_id, sensor_type, room_id, rack_id, value
FROM readings
WHERE site_id = :site_id
AND recorded_at > NOW() - INTERVAL '10 minutes'
ORDER BY sensor_id, recorded_at DESC
"""), {"site_id": site_id})
readings = result.mappings().all()
alarm_result = await session.execute(text("""
SELECT rack_id, COUNT(*) AS cnt, MAX(severity) AS worst
FROM alarms
WHERE site_id = :site_id AND state = 'active' AND rack_id IS NOT NULL
GROUP BY rack_id
"""), {"site_id": site_id})
alarm_map: dict[str, tuple[int, str]] = {
r["rack_id"]: (int(r["cnt"]), r["worst"])
for r in alarm_result.mappings().all()
}
by_sensor: dict[str, float] = {r["sensor_id"]: float(r["value"]) for r in readings}
def rack_reading(site: str, room: str, rack: str, suffix: str) -> float | None:
return by_sensor.get(f"{site}/{room}/{rack}/{suffix}")
def cooling_reading(site: str, crac: str, suffix: str) -> float | None:
return by_sensor.get(f"{site}/cooling/{crac}/{suffix}")
def ups_reading(site: str, ups: str, suffix: str) -> float | None:
return by_sensor.get(f"{site}/power/{ups}/{suffix}")
rooms = []
for room in site["rooms"]:
room_id = room["room_id"]
crac_id = room["crac_id"]
supply = cooling_reading(site_id, crac_id, "supply_temp")
return_t = cooling_reading(site_id, crac_id, "return_temp")
fan = cooling_reading(site_id, crac_id, "fan_pct")
crac_has_data = any(sid.startswith(f"{site_id}/cooling/{crac_id}") for sid in by_sensor)
if supply is not None:
crac_state = "online"
elif crac_has_data:
crac_state = "fault"
else:
crac_state = "unknown"
racks = []
for rack_id in room["racks"]:
temp = rack_reading(site_id, room_id, rack_id, "temperature")
power = rack_reading(site_id, room_id, rack_id, "power_kw")
alarm_cnt, worst_sev = alarm_map.get(rack_id, (0, None))
status = "ok"
if worst_sev == "critical" or (temp is not None and temp >= 30):
status = "critical"
elif worst_sev == "warning" or (temp is not None and temp >= 26):
status = "warning"
elif temp is None and power is None:
status = "unknown"
racks.append({
"rack_id": rack_id,
"temp": round(temp, 1) if temp is not None else None,
"power_kw": round(power, 2) if power is not None else None,
"status": status,
"alarm_count": alarm_cnt,
})
rooms.append({
"room_id": room_id,
"crac": {
"crac_id": crac_id,
"state": crac_state,
"supply_temp": round(supply, 1) if supply is not None else None,
"return_temp": round(return_t, 1) if return_t is not None else None,
"fan_pct": round(fan, 1) if fan is not None else None,
},
"racks": racks,
})
ups_units = []
for ups_id in site["ups_units"]:
charge = ups_reading(site_id, ups_id, "charge_pct")
load = ups_reading(site_id, ups_id, "load_pct")
runtime = ups_reading(site_id, ups_id, "runtime_min")
state_raw = ups_reading(site_id, ups_id, "state")
if state_raw is not None:
state = "battery" if state_raw == 1.0 else "online"
elif charge is not None:
state = "battery" if charge < 20.0 else "online"
else:
state = "unknown"
ups_units.append({
"ups_id": ups_id,
"state": state,
"charge_pct": round(charge, 1) if charge is not None else None,
"load_pct": round(load, 1) if load is not None else None,
"runtime_min": round(runtime, 0) if runtime is not None else None,
})
return {"site_id": site_id, "rooms": rooms, "ups_units": ups_units}
@router.get("/devices")
async def get_all_devices(site_id: str = Query(...)):
"""All devices across all racks for the site."""
site = TOPOLOGY.get(site_id)
if not site:
return []
devices = []
for room in site["rooms"]:
for rack_id in room["racks"]:
devices.extend(_generate_devices(site_id, room["room_id"], rack_id))
return devices
@router.get("/rack-devices")
async def get_rack_devices(site_id: str = Query(...), rack_id: str = Query(...)):
"""Devices in a specific rack."""
site = TOPOLOGY.get(site_id)
if not site:
return []
for room in site["rooms"]:
if rack_id in room["racks"]:
return _generate_devices(site_id, room["room_id"], rack_id)
return []
@router.get("/pdus")
async def get_pdus(
site_id: str = Query(...),
session: AsyncSession = Depends(get_session),
):
"""Per-rack PDU live phase data."""
site = TOPOLOGY.get(site_id)
if not site:
return []
result = await session.execute(text("""
SELECT DISTINCT ON (sensor_id)
sensor_id, sensor_type, room_id, rack_id, value
FROM readings
WHERE site_id = :site_id
AND sensor_type IN (
'power_kw', 'pdu_phase_a_kw', 'pdu_phase_b_kw', 'pdu_phase_c_kw',
'pdu_phase_a_a', 'pdu_phase_b_a', 'pdu_phase_c_a', 'pdu_imbalance'
)
AND recorded_at > NOW() - INTERVAL '5 minutes'
ORDER BY sensor_id, recorded_at DESC
"""), {"site_id": site_id})
# Build per-rack dict keyed by rack_id
rack_data: dict[str, dict] = {}
for row in result.mappings().all():
rack_id = row["rack_id"]
if not rack_id:
continue
if rack_id not in rack_data:
rack_data[rack_id] = {"rack_id": rack_id, "room_id": row["room_id"]}
field_map = {
"power_kw": "total_kw",
"pdu_phase_a_kw": "phase_a_kw",
"pdu_phase_b_kw": "phase_b_kw",
"pdu_phase_c_kw": "phase_c_kw",
"pdu_phase_a_a": "phase_a_a",
"pdu_phase_b_a": "phase_b_a",
"pdu_phase_c_a": "phase_c_a",
"pdu_imbalance": "imbalance_pct",
}
field = field_map.get(row["sensor_type"])
if field:
rack_data[rack_id][field] = round(float(row["value"]), 2)
# Emit rows for every rack in topology order, filling in None for missing data
out = []
for room in site["rooms"]:
for rack_id in room["racks"]:
d = rack_data.get(rack_id, {"rack_id": rack_id, "room_id": room["room_id"]})
imb = d.get("imbalance_pct")
status = (
"critical" if imb is not None and imb >= 10
else "warning" if imb is not None and imb >= 5
else "ok"
)
out.append({
"rack_id": rack_id,
"room_id": d.get("room_id", room["room_id"]),
"total_kw": d.get("total_kw"),
"phase_a_kw": d.get("phase_a_kw"),
"phase_b_kw": d.get("phase_b_kw"),
"phase_c_kw": d.get("phase_c_kw"),
"phase_a_a": d.get("phase_a_a"),
"phase_b_a": d.get("phase_b_a"),
"phase_c_a": d.get("phase_c_a"),
"imbalance_pct": imb,
"status": status,
})
return out