104 lines
3.6 KiB
Python
104 lines
3.6 KiB
Python
"""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)}
|