Initial commit: FastAPI JBOD monitor backend
This commit is contained in:
0
routers/__init__.py
Normal file
0
routers/__init__.py
Normal file
BIN
routers/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
routers/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
routers/__pycache__/drives.cpython-314.pyc
Normal file
BIN
routers/__pycache__/drives.cpython-314.pyc
Normal file
Binary file not shown.
BIN
routers/__pycache__/enclosures.cpython-314.pyc
Normal file
BIN
routers/__pycache__/enclosures.cpython-314.pyc
Normal file
Binary file not shown.
BIN
routers/__pycache__/overview.cpython-314.pyc
Normal file
BIN
routers/__pycache__/overview.cpython-314.pyc
Normal file
Binary file not shown.
20
routers/drives.py
Normal file
20
routers/drives.py
Normal 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
24
routers/enclosures.py
Normal 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
105
routers/overview.py
Normal 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,
|
||||
)
|
||||
Reference in New Issue
Block a user