homelab: „Rückweg testen“ — Snapshot anlegen, absichtlich rot werten, zurück, nachprüfen
Neuer Baustein „probe“ (POST /api/homelab/ziele/{id}/rueckweg-probe): legt den
Rückweg an wie vor einem Update, wertet die Prüfung absichtlich als rot, geht
zurück und prüft danach, ob das Gerät läuft und antwortet. Kein Update dabei;
Meldung grün bzw. dringend rot. Die Probe steht im Protokoll, nicht im
Update-Verlauf und nicht unter „Zuletzt eingespielt“. Der Snapshot bleibt
liegen (wie nach einem echten Rückweg).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b816fe6d32
commit
77ecb35a6d
@@ -70,7 +70,7 @@ _AKTION = {
|
||||
}
|
||||
_HOST_AKTIONEN = {"bericht", "host_update", "host_neustart", "kernel_aufraeumen"}
|
||||
_LAUF_WAS = {"app": "Update", "os": "Paket-Update", "pakete": "Paket-Update", "docker": "Docker-Update",
|
||||
"neustart": "Neustart", "kernel": "Kernel-Aufräumen"}
|
||||
"neustart": "Neustart", "kernel": "Kernel-Aufräumen", "probe": "Rückweg-Probe"}
|
||||
_SCHRITT_WORT = {"wartet": "wartet", "laeuft": "läuft", "eingespielt": "eingespielt",
|
||||
"zurueckgerollt": "zurückgerollt", "fehler": "gescheitert", "uebersprungen": "übersprungen"}
|
||||
_MELDUNG = re.compile(r"^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) "
|
||||
@@ -351,7 +351,7 @@ def _lauf(x: dict, schw: Callable) -> dict | None:
|
||||
titel = f"{_anzahl_pakete(x.get('pakete'), 'Paket')} · {name}"
|
||||
elif ergebnis == "zurueckgerollt":
|
||||
titel, stufe = f"{was} zurückgerollt · {name}", "warn"
|
||||
elif ergebnis == "fehler" and "Update nicht begonnen" in str(text or ""):
|
||||
elif ergebnis == "fehler" and ("Update nicht begonnen" in str(text or "") or "Probe nicht begonnen" in str(text or "")):
|
||||
titel, stufe = f"{was} nicht begonnen · {name}", "warn"
|
||||
elif ergebnis == "fehler":
|
||||
titel, stufe = f"{was} gescheitert · {name}", "fehler"
|
||||
@@ -359,6 +359,8 @@ def _lauf(x: dict, schw: Callable) -> dict | None:
|
||||
titel, stufe = f"Neustart ausgelöst · {name}", "info"
|
||||
elif ergebnis == "aufgeraeumt":
|
||||
titel, stufe = f"Alte Kernel entfernt · {name}", "ok"
|
||||
elif ergebnis == "probe-gruen":
|
||||
titel, stufe = f"Rückweg-Probe grün · {name}", "ok"
|
||||
elif ergebnis == "offen":
|
||||
titel, stufe = f"Docker-Probelauf · {name}", "info"
|
||||
else:
|
||||
|
||||
@@ -20,6 +20,10 @@ danach genauso mit Snapshot vorher und Rückweg bei Rot — sofern die VM freige
|
||||
Jeder Lauf steht in <Datenordner>/homelab-laeufe.json und im strukturierten Update-Verlauf. Pro Ziel
|
||||
läuft höchstens ein Lauf; startet dieser Teil neu, gilt ein offener Lauf als unterbrochen.
|
||||
|
||||
„Rückweg testen“ (Baustein „probe“, seit 25.09.2026): Snapshot bzw. Sicherung anlegen, die Prüfung absichtlich als
|
||||
rot werten, zurückgehen und nachprüfen, ob das Gerät danach läuft und antwortet. Ein Update gibt es dabei nicht; so
|
||||
lässt sich der Weg zurück jederzeit echt beweisen, ohne auf ein gescheitertes Update zu warten.
|
||||
|
||||
„Alle aktualisieren“ (sammellauf.py, seit 24.09.2026) stößt dieselben Läufe nacheinander an. Solange es läuft,
|
||||
lehnt der einzelne Knopf ab (SAMMELLAUF_SPERRE), und die Erfolgsmeldung eines Schritts entfällt — die Sammelmeldung
|
||||
am Ende sagt es; Fehler werden weiter sofort gemeldet.
|
||||
@@ -227,6 +231,34 @@ def _gast_lauf(lauf: dict, gast: dict) -> None:
|
||||
dringend=True)
|
||||
|
||||
|
||||
def _probe_lauf(lauf: dict, gast: dict) -> None:
|
||||
"""Rückweg testen: anlegen, absichtlich rot werten, zurückgehen, nachprüfen. Der Snapshot bzw. die Sicherung
|
||||
bleibt danach liegen (wie nach einem echten Rückweg) — wegräumen geht auf der Seite Snapshots."""
|
||||
vmid = gast["vmid"]
|
||||
try:
|
||||
rueckweg = _rueckweg_anlegen(lauf, gast)
|
||||
except RuntimeError as exc:
|
||||
_ende(lauf, "fehler", f"{lauf['name']}: Rückweg-Probe nicht begonnen — {str(exc).rstrip('.')}. "
|
||||
"Am Gerät wurde nichts geändert.")
|
||||
return
|
||||
if not rueckweg:
|
||||
_ende(lauf, "fehler", f"{lauf['name']}: Für dieses Gerät gibt es keinen Rückweg (weder Snapshot noch "
|
||||
"Sicherung) — die Probe ist nicht möglich.")
|
||||
return
|
||||
lauf["schritte"].append("Prüfung absichtlich auf Rot gesetzt (Probe, kein Update).")
|
||||
_merken(lauf)
|
||||
try:
|
||||
wie = _zurueck(lauf, vmid, rueckweg)
|
||||
time.sleep(WARTEN_NACH_UPDATE_S)
|
||||
fehler = pruefen(gast, _frischer_gast(lauf, vmid), "probe")
|
||||
except RuntimeError as exc:
|
||||
wie, fehler = "", [str(exc)]
|
||||
if fehler:
|
||||
_ende(lauf, "fehler", f"{lauf['name']}: Rückweg-Probe rot — {' '.join(fehler)}", dringend=True)
|
||||
return
|
||||
_ende(lauf, "probe-gruen", f"{lauf['name']}: Rückweg-Probe grün — {wie}, danach läuft das Gerät und antwortet.")
|
||||
|
||||
|
||||
def _aufraeumen(lauf: dict, vmid: int, rueckweg: tuple[str, str] | None) -> None:
|
||||
"""Ältere Snapshots des Orchestrators für diesen Gast löschen, nach einer Sicherung auch ältere Sicherungen;
|
||||
der Rückweg dieses Laufs (der neueste) bleibt. Ebenso bleiben Snapshots, die jemand auf der Seite Snapshots per
|
||||
@@ -348,13 +380,15 @@ def _ende(lauf: dict, ergebnis: str, text: str, dringend: bool = False) -> None:
|
||||
lauf.update(status="fertig", ergebnis=ergebnis, text=text, ende=time.time(), dringend=dringend,
|
||||
gemeldet=not still)
|
||||
_merken(lauf)
|
||||
try:
|
||||
# Die Schritte eines Sammellaufs stehen im Update-Verlauf als EIN Lauf (gleicher Beginn, eigener Anlass).
|
||||
start = datetime.fromtimestamp(lauf.get("sammellauf_start") or lauf["start"], LOCAL_TZ)
|
||||
anlass = "Alle aktualisieren" if lauf.get("sammellauf") else "Update von Hand"
|
||||
update_verlauf.eintragen(start.isoformat(timespec="seconds"), anlass, lauf["ziel"], ergebnis, text)
|
||||
except Exception:
|
||||
log.warning("homelab: Verlauf nicht geschrieben", exc_info=True)
|
||||
# Eine Rückweg-Probe ist kein Update: Sie steht im Protokoll, nicht im Update-Verlauf.
|
||||
if lauf.get("baustein") != "probe":
|
||||
try:
|
||||
# Die Schritte eines Sammellaufs stehen im Update-Verlauf als EIN Lauf (gleicher Beginn, eigener Anlass).
|
||||
start = datetime.fromtimestamp(lauf.get("sammellauf_start") or lauf["start"], LOCAL_TZ)
|
||||
anlass = "Alle aktualisieren" if lauf.get("sammellauf") else "Update von Hand"
|
||||
update_verlauf.eintragen(start.isoformat(timespec="seconds"), anlass, lauf["ziel"], ergebnis, text)
|
||||
except Exception:
|
||||
log.warning("homelab: Verlauf nicht geschrieben", exc_info=True)
|
||||
if not still:
|
||||
_melden(text, dringend=dringend)
|
||||
|
||||
@@ -373,13 +407,15 @@ def starten(ziel_id: str, baustein: str, sammellauf: dict | None = None) -> dict
|
||||
|
||||
sammellauf ({"id", "start"}) setzt nur „Alle aktualisieren“ (sammellauf.py) für seine Schritte; ohne ihn lehnt
|
||||
der Knopf ab, solange ein Sammellauf läuft."""
|
||||
if baustein not in ("app", "os", "pakete", "neustart", "docker", "kernel"):
|
||||
if baustein not in ("app", "os", "pakete", "neustart", "docker", "kernel", "probe"):
|
||||
return {"ok": False, "detail": "Unbekannter Baustein."}
|
||||
with START_SPERRE:
|
||||
if sammellauf is None and _sammellauf_laeuft():
|
||||
return {"ok": False, "detail": SAMMELLAUF_SPERRE}
|
||||
if laeuft(ziel_id):
|
||||
return {"ok": False, "detail": "Für dieses Gerät läuft schon ein Update."}
|
||||
if ziel_id == "pve" and baustein == "probe":
|
||||
return {"ok": False, "detail": "Für den Proxmox-Host gibt es keinen Rückweg zum Testen."}
|
||||
if ziel_id == "pve":
|
||||
name, gast = "Proxmox-Host", None
|
||||
host = ((kanal.bericht() or {}).get("bericht") or {}).get("host") or {}
|
||||
@@ -402,6 +438,13 @@ def starten(ziel_id: str, baustein: str, sammellauf: dict | None = None) -> dict
|
||||
return {"ok": False, "detail": "Dieses Gerät ist nicht freigegeben (Etikett watcher fehlt)."}
|
||||
app = apps.app_fuer((gast.get("app") or {}).get("kennung"), gast.get("name"))
|
||||
name = app.name if app else str(gast.get("name"))
|
||||
if baustein == "probe":
|
||||
if not (gast.get("snapshot_moeglich") or gast.get("sicherung_moeglich")):
|
||||
return {"ok": False, "detail": f"{name} hat keinen Rückweg (weder Snapshot noch Sicherung)."}
|
||||
lauf = _neuer_lauf(ziel_id, baustein, name, None)
|
||||
threading.Thread(target=_probe_lauf, args=(lauf, gast), name=f"homelab-{ziel_id}",
|
||||
daemon=True).start()
|
||||
return {"ok": True, "lauf": lauf["id"]}
|
||||
if baustein == "app" and (not app or app.weg != "skript"):
|
||||
return {"ok": False, "detail": f"{name} aktualisiert sich nicht über das Community-Script "
|
||||
"(eigene Oberfläche oder Pakete)."}
|
||||
|
||||
Reference in New Issue
Block a user