homelab: „Alle aktualisieren“, Protokoll und Arcane-Schlüssel über die Oberfläche

- Sammellauf (services/homelab/sammellauf.py): alle Updates mit Knopf nacheinander über updates.starten,
  Gäste nach VMID, dann NPMplus und AdGuard, zuletzt die Host-Pakete (nie der Neustart). Rot stoppt den Rest
  (übersprungen, abgebrochen, dringende Meldung); grün gibt eine Sammelmeldung, die Erfolgsmeldungen der
  Schritte bleiben so lange aus. Nur einer gleichzeitig, der einzelne Knopf lehnt solange ab (START_SPERRE).
  Ein Neustart mitten im Lauf macht ihn beim Start zu „abgebrochen“, „unterbrochen (Neustart)“.
- Protokoll (services/homelab/protokoll.py): Aufträge, Läufe, Sammelläufe, Wächter-Verlauf, Melde-Log und
  wöchentliches Suchen, neueste zuerst, ohne Geheimnisse. Der Kanal hebt 500 erledigte Aufträge auf und
  vermerkt eingehende Berichte; die Pflege führt eine kurze Ereignisliste, der Wächter-Verlauf die Stufe.
- Einstellungen (services/homelab/einstellungen.py): Arcane-Schlüssel erst gegen Arcane prüfen, dann in
  arcane.key (0600) speichern, nie zurückgeben; echte Docker-Updates per Schalter. Die Umgebung geht vor.
- Schnittstellen als eigener Block in routers/homelab.py vor den Ausführer-Endpunkten.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-25 10:09:14 +02:00
co-authored by Claude Opus 5.5
parent bf1153cb66
commit 8ec261b8b2
15 changed files with 2329 additions and 60 deletions
+24 -2
View File
@@ -15,7 +15,9 @@ Die Übersicht ist nur so ehrlich wie die Paketlisten in den Gästen, und die er
Der Steward der Rolle homelab ruft pruefe_paketlisten() in jedem Wächter-Takt auf (services/waechter.py,
PRUEFUNGEN). Zustand in <Datenordner>/homelab-pflege.json; den schreibt nur der Steward. Die Aufträge selbst
landen im Kanal (kanal.py, unter Dateisperre, weil auch die Oberfläche dort schreibt).
landen im Kanal (kanal.py, unter Dateisperre, weil auch die Oberfläche dort schreibt). Seit 24.09.2026 steht dort
auch eine kurze Ereignisliste (angestoßen, gescheitert, wieder ok; die letzten EREIGNISSE_MAX) — das Protokoll der
Oberfläche liest sie (protokoll.py).
"""
import json
@@ -35,6 +37,7 @@ log = logging.getLogger(__name__)
LISTEN_ALT_S = 7 * 24 * 3600
NACHT_VON, NACHT_BIS = 2, 5 # volle Stunden, Europe/Berlin
FEHLSCHLAEGE_HINWEIS = 2
EREIGNISSE_MAX = 200
_lock = threading.Lock()
@@ -82,10 +85,21 @@ def _fehlerzeile(text: str | None) -> str:
return (wichtig or zeilen or ["ohne Angabe"])[-1][:200]
def _ereignis(daten: dict, ts: float, vmid: object, name: object, art: str, text: str) -> None:
"""Für das Protokoll der Oberfläche; die Liste bleibt kurz."""
try:
vmid = int(vmid)
except (TypeError, ValueError):
return
liste = daten["ereignisse"] if isinstance(daten.get("ereignisse"), list) else []
liste.append({"ts": ts, "vmid": vmid, "name": str(name or vmid), "art": art, "text": text})
daten["ereignisse"] = liste[-EREIGNISSE_MAX:]
def _einsammeln(daten: dict) -> bool:
"""Ergebnisse der eigenen Aufträge übernehmen: fertig → Zähler auf null, gescheitert → eins mehr."""
geaendert = False
for eintrag in daten["gaeste"].values():
for vmid, eintrag in daten["gaeste"].items():
aid = eintrag.get("auftrag")
if not aid:
continue
@@ -93,10 +107,15 @@ def _einsammeln(daten: dict) -> bool:
if a is not None and a.get("status") in ("wartet", "laeuft"):
continue
if a is not None and a.get("status") == "fertig":
if eintrag.get("fehlschlaege"):
_ereignis(daten, a.get("fertig") or time.time(), vmid, eintrag.get("name"), "wieder_ok",
"Die Paketlisten ließen sich wieder erneuern.")
eintrag["fehlschlaege"], eintrag["fehler"] = 0, None
elif a is not None: # fehler, verloren
eintrag["fehlschlaege"] = int(eintrag.get("fehlschlaege") or 0) + 1
eintrag["fehler"] = _fehlerzeile(a.get("text"))
_ereignis(daten, a.get("fertig") or time.time(), vmid, eintrag.get("name"), "gescheitert",
f"{eintrag['fehlschlaege']}. Fehlschlag in Folge: {eintrag['fehler']}")
eintrag["auftrag"] = None # nicht mehr in der Liste: vergessen, nicht zählen
geaendert = True
return geaendert
@@ -173,6 +192,9 @@ def pruefe_paketlisten(jetzt: float | None = None) -> list:
log.info("pflege (trocken): würde die Paketlisten von %s erneuern", eintrag["name"])
continue
eintrag.update(tag=heute, auftrag=kanal.anlegen("suchen", {"vmid": vmid}), angestossen=jetzt)
listen = (g.get("os_updates") or {}).get("listen_stand")
_ereignis(daten, jetzt, vmid, eintrag["name"], "angestossen",
f"Die Paketlisten sind vom {datetime.fromtimestamp(listen, LOCAL_TZ):%d.%m.%Y}.")
geaendert = True
log.info("pflege: Paketlisten von %s sind alt → suchen", eintrag["name"])
if geaendert: