homelab: Arcane-Docker ehrlich zum Rückweg – Snapshot der VM nur mit Etikett watcher

Die Update-Liste zeigte bei Arcane „Snapshot vorher, bei Rot zurück“, obwohl das
Docker-Update keinen Snapshot anlegte (und der Ausführer VM 106 ohne Etikett gar
nicht anfassen darf). Jetzt:
- Gäste ohne Freigabe haben Rückweg „keiner“, Rückfrage und Plan sagen es.
- Echte Docker-Updates legen vorher einen Snapshot der VM an, wenn sie freigegeben
  ist, prüfen danach (Fehler-Container, Arcane antwortet binnen 3 min) und gehen
  bei Rot zurück – wie bei den Containern.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-25 10:29:07 +02:00
co-authored by Claude Opus 5.5
parent c37c0dccbb
commit 67ed0458a9
7 changed files with 117 additions and 25 deletions
+15 -5
View File
@@ -9,7 +9,8 @@ Wie ehrlich der Stand ist, steht dabei: Sind die Paketlisten im Gast älter als
„unbekannt“ (mit Knopf zum Suchen) statt „aktuell“; ist der Bericht älter als eine halbe Stunde, sagt
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.
wo keiner geht eine Sicherung, sonst keiner. Ohne Freigabe (Etikett) legt der Ausführer weder das eine noch
das andere an — das betrifft Arcane, dessen Docker-Updates auch ohne Etikett über Arcane selbst laufen.
"""
import re
@@ -28,6 +29,7 @@ BERICHT_ALT_S = 30 * 60
LISTEN_ALT_S = 14 * 24 * 3600
EIGENES_ETIKETT = "mc2"
ERREICHBAR_CACHE_S = 60
OHNE_FREIGABE = "Das Etikett „watcher“ fehlt, der Ausführer darf hier keinen Snapshot anlegen"
_erreichbar_cache: dict[str, tuple[float, bool]] = {}
_erreichbar_lock = threading.Lock()
@@ -74,6 +76,8 @@ def _datum(ts: float | None) -> str:
def _rueckweg(gast: dict) -> str:
"""Die Zeile „Rückweg“ der Karte: was vor jedem Update geschieht."""
if not gast.get("erlaubt"):
return f"Kein automatischer Rückweg: {OHNE_FREIGABE}."
if gast.get("snapshot_moeglich"):
return "Snapshot vor jedem Update"
grund = gast.get("snapshot_grund") or "kein Snapshot möglich"
@@ -86,6 +90,8 @@ def _rueckweg(gast: dict) -> str:
def _rueckweg_art(gast: dict) -> str:
"""Dasselbe in einem Wort — die Oberfläche fasst den Rückweg in der Update-Liste kurz."""
if not gast.get("erlaubt"):
return "keiner"
if gast.get("snapshot_moeglich"):
return "snapshot"
return "sicherung" if gast.get("sicherung_moeglich") else "keiner"
@@ -93,6 +99,8 @@ def _rueckweg_art(gast: dict) -> str:
def _rueckweg_frage(gast: dict) -> str:
"""Der Satz zum Rückweg in der Rückfrage vor „Jetzt updaten“."""
if not gast.get("erlaubt"):
return f"{OHNE_FREIGABE} — scheitert das Update, gibt es keinen automatischen Rückweg."
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"):
@@ -166,7 +174,7 @@ def _os_baustein(gast: dict, zid: str, jetzt: float) -> BausteinStand:
return stand
def _arcane_bausteine(basis: str | None, zid: str) -> list[BausteinStand]:
def _arcane_bausteine(basis: str | None, zid: str, gast: dict) -> list[BausteinStand]:
"""Arcane selbst (öffentliche Version) und die Docker-Images (mit API-Schlüssel)."""
app = arcane.app_version(basis) if basis else None
selbst = BausteinStand(id="arcane", name="Arcane", zustand="unbekannt",
@@ -195,7 +203,8 @@ def _arcane_bausteine(basis: str | None, zid: str) -> list[BausteinStand]:
docker.aktion = Aktion(
"POST", f"/api/homelab/ziele/{zid}/docker",
("Arcane prüft im Probelauf, welche Container es aktualisieren würde — geändert wird noch nichts."
if probe else "Arcane aktualisiert jetzt die Container mit neuen Images; Daten-Volumes bleiben."),
if probe else "Arcane aktualisiert jetzt die Container mit neuen Images; Daten-Volumes bleiben. "
+ _rueckweg_frage(gast)),
label="Probelauf" if probe else "Jetzt updaten")
else:
docker.zustand = "aktuell"
@@ -216,8 +225,9 @@ def _gast_ziel(gast: dict, jetzt: float) -> ZielStand:
grund=f"Der Gast ist {gast.get('status') or 'unbekannt'}."))
return ziel
if app and app.kennung == "arcane":
# Arcane läuft über seine eigene Schnittstelle, nicht über den Ausführer: kein Etikett nötig.
ziel.bausteine += _arcane_bausteine(url.rstrip("/") if url else None, zid)
# Arcane läuft über seine eigene Schnittstelle, nicht über den Ausführer: kein Etikett nötig — nur für den
# Snapshot vorher (Rückweg), den legt der Ausführer an.
ziel.bausteine += _arcane_bausteine(url.rstrip("/") if url else None, zid, gast)
return ziel
if not gast.get("erlaubt"):
ziel.bausteine.append(BausteinStand(