"""Host / Docker system information.""" from __future__ import annotations import os import shutil from fastapi import APIRouter, Depends, HTTPException, Request from sqlmodel import Session from auth import get_current_user, require_admin from config import settings from database import get_session from docker_client import DockerError, get_client, safe_call from models.user import User from services import audit_service, device_service, gpu_service, self_update_service router = APIRouter(prefix="/api/system", tags=["system"]) def _read_proc(path: str) -> str: full = os.path.join(settings.HOST_PROC_PATH, path) if not os.path.isfile(full): full = os.path.join("/proc", path) try: with open(full, "r", encoding="utf-8") as fh: return fh.read() except OSError: return "" def _mem_info() -> dict: info = {} for line in _read_proc("meminfo").splitlines(): parts = line.split(":") if len(parts) == 2: key = parts[0].strip() val = parts[1].strip().split()[0] try: info[key] = int(val) * 1024 # kB -> bytes except ValueError: pass total = info.get("MemTotal", 0) available = info.get("MemAvailable", info.get("MemFree", 0)) return {"total": total, "available": available, "used": max(total - available, 0)} def _uptime() -> float: raw = _read_proc("uptime") try: return float(raw.split()[0]) except (IndexError, ValueError): return 0.0 def _cpu_count() -> int: return os.cpu_count() or 0 def _disk_usage() -> dict: try: usage = shutil.disk_usage(settings.DATA_DIR) return {"total": usage.total, "used": usage.used, "free": usage.free} except OSError: return {"total": 0, "used": 0, "free": 0} @router.get("/info") def system_info(_user: User = Depends(get_current_user)) -> dict: docker_version = "" host_os = "" containers_running = 0 containers_total = 0 try: client = get_client() version = safe_call(client.version) docker_version = version.get("Version", "") info = safe_call(client.info) host_os = info.get("OperatingSystem", "") containers_running = info.get("ContainersRunning", 0) containers_total = info.get("Containers", 0) except DockerError as exc: docker_version = f"unavailable ({exc.error})" return { "docker_version": docker_version, "host_os": host_os, "hostname": os.uname().nodename, "cpu_cores": _cpu_count(), "ram": _mem_info(), "disk": _disk_usage(), "uptime_seconds": _uptime(), "containers_running": containers_running, "containers_total": containers_total, "gpus": [g.to_dict() for g in gpu_service.detect_gpus()], } @router.get("/gpus") def gpus(_user: User = Depends(get_current_user)) -> list[dict]: """Re-detect GPUs live.""" return [g.to_dict() for g in gpu_service.detect_gpus()] @router.get("/devices") def devices(_user: User = Depends(get_current_user)) -> dict: """List host USB / serial / DRI devices for passthrough.""" return device_service.detect_devices() @router.get("/update") async def self_update_status( refresh: bool = False, _user: User = Depends(get_current_user), ) -> dict: """Is a newer StackPilot release available? (registry check, cached)""" return await self_update_service.get_status(refresh=refresh) @router.post("/update") def self_update_apply( request: Request, session: Session = Depends(get_session), user: User = Depends(require_admin), ) -> dict: """Update this StackPilot in place via a detached compose helper.""" try: result = self_update_service.apply_update() except self_update_service.SelfUpdateError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc audit_service.record( session, user=user.username, action="system.update", target=result.get("helper", ""), detail=result.get("command"), ip=request.client.host if request.client else "", ) return result