"""Schnittstellen des Homelab-Teils (Rolle homelab, Phase 3/4, 24.09.2026). GET /api/homelab/ziele Proxmox-Host und Gäste im gemeinsamen Ziel-Modell POST /api/homelab/ziele/{id}/update „Jetzt updaten“ (App bzw. Host-Pakete) POST /api/homelab/ziele/{id}/os-update Pakete im Gast einspielen POST /api/homelab/ziele/{id}/suchen Paketlisten im Gast erneuern POST /api/homelab/ziele/{id}/docker Docker über Arcane (zuerst Probelauf) POST /api/homelab/ziele/pve/neustart Proxmox-Host neu starten (eigener Knopf) GET /api/homelab/laeufe Die letzten Update-Läufe mit ihren Schritten GET /api/homelab/ausfuehrer Lebenszeichen des Ausführers GET /api/homelab/ausfuehrer/auftrag ┐ POST /api/homelab/ausfuehrer/bericht ├ nur für den Ausführer (Kopfzeile X-MC2-Ausfuehrer) POST /api/homelab/ausfuehrer/ergebnis/{id} ┘ Dünn: die Logik liegt in services/homelab/. """ import time from fastapi import APIRouter, Depends, Header, HTTPException from pydantic import BaseModel from services.homelab import inventar, kanal, updates router = APIRouter(prefix="/api/homelab", tags=["homelab"]) def _nur_ausfuehrer(x_mc2_ausfuehrer: str | None = Header(default=None)) -> None: if not kanal.token_gueltig(x_mc2_ausfuehrer): raise HTTPException(status_code=403, detail="Nur für den Ausführer auf dem Proxmox-Host.") kanal.kontakt() @router.get("/ziele") def ziele() -> dict: return inventar.ziele() def _antwort(ergebnis: dict) -> dict: if not ergebnis.get("ok"): raise HTTPException(status_code=409, detail=ergebnis.get("detail") or "Das geht gerade nicht.") return ergebnis @router.post("/ziele/{ziel_id}/update") def update(ziel_id: str) -> dict: return _antwort(updates.starten(ziel_id, "pakete" if ziel_id == "pve" else "app")) @router.post("/ziele/{ziel_id}/os-update") def os_update(ziel_id: str) -> dict: return _antwort(updates.starten(ziel_id, "os")) @router.post("/ziele/{ziel_id}/docker") def docker(ziel_id: str) -> dict: """Docker über Arcanes Updater — zuerst nur als Probelauf (MC_ARCANE_ECHT=1 macht es echt).""" return _antwort(updates.starten(ziel_id, "docker")) @router.post("/ziele/pve/neustart") def host_neustart() -> dict: return _antwort(updates.starten("pve", "neustart")) @router.post("/ziele/{ziel_id}/suchen") def suchen(ziel_id: str) -> dict: gast = inventar.gast(ziel_id) if not gast or not gast.get("erlaubt"): raise HTTPException(status_code=404, detail="Dieses Gerät steht nicht (freigegeben) im Bericht.") return {"ok": True, "auftrag": kanal.anlegen("suchen", {"vmid": gast["vmid"]})} @router.get("/laeufe") def laeufe() -> dict: return {"laeufe": updates.laeufe()} @router.get("/ausfuehrer") def ausfuehrer() -> dict: zuletzt = kanal.zuletzt() return {"zuletzt": zuletzt, "verbunden": bool(zuletzt and time.time() - zuletzt < inventar.BERICHT_ALT_S), "auftraege": kanal.liste()[:20]} @router.get("/ausfuehrer/auftrag", dependencies=[Depends(_nur_ausfuehrer)]) def auftrag_abholen() -> dict: return kanal.naechster() or {} @router.post("/ausfuehrer/bericht", dependencies=[Depends(_nur_ausfuehrer)]) def bericht(daten: dict) -> dict: kanal.bericht_speichern(daten) return {"ok": True} class Ergebnis(BaseModel): code: int text: str = "" @router.post("/ausfuehrer/ergebnis/{auftrag_id}", dependencies=[Depends(_nur_ausfuehrer)]) def ergebnis(auftrag_id: str, e: Ergebnis) -> dict: return {"ok": kanal.ergebnis(auftrag_id, e.code, e.text)}