Initial commit: FastAPI JBOD monitor backend

This commit is contained in:
2026-03-07 02:14:17 +00:00
commit 9f918a3308
26 changed files with 651 additions and 0 deletions

0
routers/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

20
routers/drives.py Normal file
View File

@@ -0,0 +1,20 @@
from fastapi import APIRouter, HTTPException
from models.schemas import DriveDetail
from services.smart import get_smart_data
router = APIRouter(prefix="/api/drives", tags=["drives"])
@router.get("/{device}", response_model=DriveDetail)
async def get_drive_detail(device: str):
"""Get SMART detail for a specific block device."""
try:
data = await get_smart_data(device)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
if "error" in data:
raise HTTPException(status_code=502, detail=data["error"])
return DriveDetail(**data)

24
routers/enclosures.py Normal file
View File

@@ -0,0 +1,24 @@
from fastapi import APIRouter, HTTPException
from models.schemas import Enclosure, SlotInfo
from services.enclosure import discover_enclosures, list_slots
router = APIRouter(prefix="/api/enclosures", tags=["enclosures"])
@router.get("", response_model=list[Enclosure])
async def get_enclosures():
"""Discover all SES enclosures."""
return discover_enclosures()
@router.get("/{enclosure_id}/drives", response_model=list[SlotInfo])
async def get_enclosure_drives(enclosure_id: str):
"""List all drive slots for an enclosure."""
slots = list_slots(enclosure_id)
if not slots:
# Check if the enclosure exists at all
enclosures = discover_enclosures()
if not any(e["id"] == enclosure_id for e in enclosures):
raise HTTPException(status_code=404, detail=f"Enclosure '{enclosure_id}' not found")
return slots

105
routers/overview.py Normal file
View File

@@ -0,0 +1,105 @@
import asyncio
import logging
from fastapi import APIRouter
from models.schemas import (
DriveHealthSummary,
EnclosureWithDrives,
Overview,
SlotWithDrive,
)
from services.enclosure import discover_enclosures, list_slots
from services.smart import get_smart_data
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/overview", tags=["overview"])
@router.get("", response_model=Overview)
async def get_overview():
"""Aggregate view of all enclosures, slots, and drive health."""
enclosures_raw = discover_enclosures()
enc_results: list[EnclosureWithDrives] = []
total_drives = 0
warnings = 0
errors = 0
all_healthy = True
for enc in enclosures_raw:
slots_raw = list_slots(enc["id"])
# Gather SMART data for all populated slots concurrently
populated = [(s, s["device"]) for s in slots_raw if s["populated"] and s["device"]]
smart_tasks = [get_smart_data(dev) for _, dev in populated]
smart_results = await asyncio.gather(*smart_tasks, return_exceptions=True)
smart_map: dict[str, dict] = {}
for (slot_info, dev), result in zip(populated, smart_results):
if isinstance(result, Exception):
logger.warning("SMART query failed for %s: %s", dev, result)
smart_map[dev] = {"device": dev, "smart_supported": False}
else:
smart_map[dev] = result
slots_out: list[SlotWithDrive] = []
for s in slots_raw:
drive_summary = None
if s["device"] and s["device"] in smart_map:
sd = smart_map[s["device"]]
total_drives += 1
healthy = sd.get("smart_healthy")
if healthy is False:
errors += 1
all_healthy = False
elif healthy is None and sd.get("smart_supported", True):
warnings += 1
# Check for concerning SMART values
if sd.get("reallocated_sectors") and sd["reallocated_sectors"] > 0:
warnings += 1
if sd.get("pending_sectors") and sd["pending_sectors"] > 0:
warnings += 1
if sd.get("uncorrectable_errors") and sd["uncorrectable_errors"] > 0:
warnings += 1
drive_summary = DriveHealthSummary(
device=sd["device"],
model=sd.get("model"),
serial=sd.get("serial"),
smart_healthy=healthy,
smart_supported=sd.get("smart_supported", True),
temperature_c=sd.get("temperature_c"),
power_on_hours=sd.get("power_on_hours"),
)
elif s["populated"]:
total_drives += 1
slots_out.append(SlotWithDrive(
slot=s["slot"],
populated=s["populated"],
device=s["device"],
drive=drive_summary,
))
enc_results.append(EnclosureWithDrives(
id=enc["id"],
sg_device=enc.get("sg_device"),
vendor=enc["vendor"],
model=enc["model"],
revision=enc["revision"],
total_slots=enc["total_slots"],
populated_slots=enc["populated_slots"],
slots=slots_out,
))
return Overview(
healthy=all_healthy and errors == 0,
drive_count=total_drives,
warning_count=warnings,
error_count=errors,
enclosures=enc_results,
)