homelab: Wartezeit nach Skriptaenderung, PBS mit Sicherung statt Snapshot, woechentliches Suchen

- karenz.py: ct/<app>.sh juenger als MC_HOMELAB_KARENZ_H (Standard 48 h) -> Baustein "neu" ohne Knopf,
  updates.starten lehnt mit demselben Satz ab (Berliner Zeit). GitHub stumm -> nicht blockieren, die
  Rueckfrage sagt es.
- Ausfuehrer: neue Aktionen sichern, sicherung_zurueck, sicherung_loeschen. vzdump auf den ersten lokalen
  Speicher mit Inhalt backup (oder sicherung_speicher aus /etc/mc2-ausfuehrer.json), nie auf pbs; vorher
  Platz pruefen (frei > belegt x 1,2); Notiz mc2-sicherung, nur solche werden zurueckgespielt/geloescht;
  Rueckweg: stoppen, pct restore --force auf den bisherigen rootfs-Speicher, starten. Bericht mit
  host.sicherung, sicherung_moeglich/_grund, eigenen Sicherungen und nur_lesen. Unerwartete Fehler werden
  beantwortet statt verschluckt; vzdump/restore beim Zeitlimit erst SIGTERM.
- updates.py: Sicherung, wo kein Snapshot geht; bei Rot zurueckspielen, nach Gruen aeltere Sicherungen
  weg. Scheitert Snapshot oder Sicherung, beginnt das Update nicht (nicht dringend gemeldet).
- pflege.py: "suchen" fuer Gaeste mit Paketlisten aelter als 7 Tage, nachts 02-05 Uhr (sonst nachholen),
  einmal je Gast und Tag, nie neben einem Update oder offenen Auftrag; gelber Waechter-Hinweis, wenn es
  zweimal hintereinander scheitert. Eine Registrierungszeile in waechter.py.
- kanal.py: Auftragsliste unter flock, weil jetzt auch der Steward Auftraege anlegt.
- inventar.py: Rueckweg und Rueckfrage fuer die Sicherung; Gaeste mitten im Update-Lauf nicht in der
  Webpruefung des Waechters (sonst zweiter Alarm beim Zurueckspielen).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-24 20:22:39 +02:00
co-authored by Claude Opus 5.5
parent 40d2840b16
commit f2733f44fe
13 changed files with 1385 additions and 54 deletions
+46 -12
View File
@@ -7,7 +7,9 @@ Dazu prüft dieser Teil selbst, ob die Weboberfläche jedes Gasts antwortet.
Wie ehrlich der Stand ist, steht dabei: Sind die Paketlisten im Gast älter als zwei Wochen, heißt es
„unbekannt“ (mit Knopf zum Suchen) statt „aktuell“; ist der Bericht älter als eine halbe Stunde, sagt
die Übersicht, dass der Ausführer schweigt.
die Übersicht, dass der Ausführer schweigt. Ist das Update-Skript einer App frisch geändert, bleibt sie
„neu“, aber ohne Knopf (karenz.py). Rückweg und Rückfrage sagen, was vor dem Update geschieht: Snapshot,
wo keiner geht eine Sicherung, sonst keiner.
"""
import re
@@ -20,7 +22,7 @@ from kern.github import neueste_version
from kern.zeit import LOCAL_TZ
from kern.ziele import Aktion, BausteinStand, ZielStand
from services.homelab import apps, arcane, kanal
from services.homelab import apps, arcane, kanal, karenz
BERICHT_ALT_S = 30 * 60
LISTEN_ALT_S = 14 * 24 * 3600
@@ -70,6 +72,30 @@ def _datum(ts: float | None) -> str:
return datetime.fromtimestamp(ts, LOCAL_TZ).strftime("%d.%m.%Y") if ts else "?"
def _rueckweg(gast: dict) -> str:
"""Die Zeile „Rückweg“ der Karte: was vor jedem Update geschieht."""
if gast.get("snapshot_moeglich"):
return "Snapshot vor jedem Update"
grund = gast.get("snapshot_grund") or "kein Snapshot möglich"
if gast.get("sicherung_moeglich"):
return f"Sicherung auf „{gast.get('sicherung_speicher') or 'dem Host'}“ vor jedem Update ({grund})"
if gast.get("sicherung_grund"):
return f"Kein automatischer Rückweg: {grund}. Auch keine Sicherung: {gast['sicherung_grund']}"
return grund
def _rueckweg_frage(gast: dict) -> str:
"""Der Satz zum Rückweg in der Rückfrage vor „Jetzt updaten“."""
if gast.get("snapshot_moeglich"):
return "Vorher wird ein Snapshot angelegt; ist die Prüfung danach rot, geht es von selbst zurück."
if gast.get("sicherung_moeglich"):
return "Vorher wird eine Sicherung angelegt; ist die Prüfung danach rot, wird sie zurückgespielt."
satz = "Ein Snapshot ist hier nicht möglich — scheitert das Update, gibt es keinen automatischen Rückweg."
if gast.get("sicherung_grund"):
satz += f" Eine Sicherung geht auch nicht: {gast['sicherung_grund']}"
return satz
def _app_baustein(gast: dict, app: apps.App, zid: str) -> BausteinStand:
installiert = (gast.get("app") or {}).get("version")
stand = BausteinStand(id="app", name=app.name, zustand="aktuell", installiert=installiert)
@@ -103,10 +129,15 @@ def _app_baustein(gast: dict, app: apps.App, zid: str) -> BausteinStand:
stand.grund = f"{app.name} aktualisiert sich über die eigene Oberfläche; das Community-Script tut es nicht."
elif ist_neuer:
stand.zustand, stand.kurz = "neu", f"{installiert} → {neu['version']}"
rueckweg = ("Vorher wird ein Snapshot angelegt; ist die Prüfung danach rot, geht es von selbst zurück."
if gast.get("snapshot_moeglich") else
"Ein Snapshot ist hier nicht möglich — scheitert das Update, gibt es keinen automatischen Rückweg.")
stand.aktion = Aktion("POST", f"/api/homelab/ziele/{zid}/update", f"{app.name} jetzt aktualisieren? {rueckweg}")
# Frisch geänderte Update-Skripte erst nach der Wartezeit (karenz.py): „neu“, aber ohne Knopf.
wartezeit = karenz.pruefen(app.kennung)
if wartezeit["gesperrt"]:
stand.grund = wartezeit["gesperrt"]
return stand
frage = f"{app.name} jetzt aktualisieren? {_rueckweg_frage(gast)}"
if not wartezeit["geprueft"]:
frage += f" {karenz.NICHT_GEPRUEFT}"
stand.aktion = Aktion("POST", f"/api/homelab/ziele/{zid}/update", frage)
return stand
@@ -124,7 +155,7 @@ def _os_baustein(gast: dict, zid: str, jetzt: float) -> BausteinStand:
elif anzahl > 0:
stand.zustand, stand.kurz = "neu", f"{anzahl} Pakete"
stand.aktion = Aktion("POST", f"/api/homelab/ziele/{zid}/os-update",
f"{anzahl} Pakete im Gast jetzt einspielen? Vorher wird, wo möglich, ein Snapshot angelegt.")
f"{anzahl} Pakete im Gast jetzt einspielen? {_rueckweg_frage(gast)}")
return stand
@@ -172,8 +203,7 @@ def _gast_ziel(gast: dict, jetzt: float) -> ZielStand:
laeuft = gast.get("status") == "running"
ziel = ZielStand(id=zid, name=app.name if app else str(gast.get("name") or zid), art=art, instanz="homelab",
erreichbar=(erreichbar(url) if laeuft else False) if url else (laeuft or None),
rueckweg=("Snapshot vor jedem Update" if gast.get("snapshot_moeglich")
else gast.get("snapshot_grund") or "kein Snapshot möglich"))
rueckweg=_rueckweg(gast))
if not laeuft:
ziel.bausteine.append(BausteinStand(id="status", name="Status", zustand="unbekannt",
grund=f"Der Gast ist {gast.get('status') or 'unbekannt'}."))
@@ -238,14 +268,18 @@ def ziele() -> dict:
def erreichbarkeit() -> list[dict]:
"""Für den Wächter: jede Weboberfläche laufender, freigegebener Gäste — ohne GitHub-Abfragen."""
"""Für den Wächter: jede Weboberfläche laufender, freigegebener Gäste — ohne GitHub-Abfragen. Gäste mitten in
einem Update-Lauf bleiben außen vor: Sicherung, Update und Zurückspielen legen sie kurz still, und was dabei
herauskommt, meldet der Lauf selbst (sonst käme ein zweiter Alarm samt „wieder ok“)."""
from services.homelab import updates # updates importiert dieses Modul
eingang = kanal.bericht()
im_update = updates.laufende_ziele()
ergebnis = []
for g in (eingang or {}).get("bericht", {}).get("gaeste") or []:
app = apps.app_fuer((g.get("app") or {}).get("kennung"), g.get("name"))
url = apps.adresse(app, g.get("ip"))
if url and g.get("erlaubt"):
zid = f"{'ct' if g['art'] == 'lxc' else 'vm'}-{g['vmid']}"
zid = f"{'ct' if g['art'] == 'lxc' else 'vm'}-{g['vmid']}"
if url and g.get("erlaubt") and zid not in im_update:
ergebnis.append({"id": zid, "name": app.name if app else g.get("name"), "url": url,
"laeuft": g.get("status") == "running",
"ok": g.get("status") == "running" and erreichbar(url)})