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:
co-authored by
Claude Opus 5.5
parent
c37c0dccbb
commit
67ed0458a9
@@ -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
|
„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
|
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,
|
„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
|
import re
|
||||||
@@ -28,6 +29,7 @@ BERICHT_ALT_S = 30 * 60
|
|||||||
LISTEN_ALT_S = 14 * 24 * 3600
|
LISTEN_ALT_S = 14 * 24 * 3600
|
||||||
EIGENES_ETIKETT = "mc2"
|
EIGENES_ETIKETT = "mc2"
|
||||||
ERREICHBAR_CACHE_S = 60
|
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_cache: dict[str, tuple[float, bool]] = {}
|
||||||
_erreichbar_lock = threading.Lock()
|
_erreichbar_lock = threading.Lock()
|
||||||
@@ -74,6 +76,8 @@ def _datum(ts: float | None) -> str:
|
|||||||
|
|
||||||
def _rueckweg(gast: dict) -> str:
|
def _rueckweg(gast: dict) -> str:
|
||||||
"""Die Zeile „Rückweg“ der Karte: was vor jedem Update geschieht."""
|
"""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"):
|
if gast.get("snapshot_moeglich"):
|
||||||
return "Snapshot vor jedem Update"
|
return "Snapshot vor jedem Update"
|
||||||
grund = gast.get("snapshot_grund") or "kein Snapshot möglich"
|
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:
|
def _rueckweg_art(gast: dict) -> str:
|
||||||
"""Dasselbe in einem Wort — die Oberfläche fasst den Rückweg in der Update-Liste kurz."""
|
"""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"):
|
if gast.get("snapshot_moeglich"):
|
||||||
return "snapshot"
|
return "snapshot"
|
||||||
return "sicherung" if gast.get("sicherung_moeglich") else "keiner"
|
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:
|
def _rueckweg_frage(gast: dict) -> str:
|
||||||
"""Der Satz zum Rückweg in der Rückfrage vor „Jetzt updaten“."""
|
"""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"):
|
if gast.get("snapshot_moeglich"):
|
||||||
return "Vorher wird ein Snapshot angelegt; ist die Prüfung danach rot, geht es von selbst zurück."
|
return "Vorher wird ein Snapshot angelegt; ist die Prüfung danach rot, geht es von selbst zurück."
|
||||||
if gast.get("sicherung_moeglich"):
|
if gast.get("sicherung_moeglich"):
|
||||||
@@ -166,7 +174,7 @@ def _os_baustein(gast: dict, zid: str, jetzt: float) -> BausteinStand:
|
|||||||
return stand
|
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)."""
|
"""Arcane selbst (öffentliche Version) und die Docker-Images (mit API-Schlüssel)."""
|
||||||
app = arcane.app_version(basis) if basis else None
|
app = arcane.app_version(basis) if basis else None
|
||||||
selbst = BausteinStand(id="arcane", name="Arcane", zustand="unbekannt",
|
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(
|
docker.aktion = Aktion(
|
||||||
"POST", f"/api/homelab/ziele/{zid}/docker",
|
"POST", f"/api/homelab/ziele/{zid}/docker",
|
||||||
("Arcane prüft im Probelauf, welche Container es aktualisieren würde — geändert wird noch nichts."
|
("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")
|
label="Probelauf" if probe else "Jetzt updaten")
|
||||||
else:
|
else:
|
||||||
docker.zustand = "aktuell"
|
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'}."))
|
grund=f"Der Gast ist {gast.get('status') or 'unbekannt'}."))
|
||||||
return ziel
|
return ziel
|
||||||
if app and app.kennung == "arcane":
|
if app and app.kennung == "arcane":
|
||||||
# Arcane läuft über seine eigene Schnittstelle, nicht über den Ausführer: kein Etikett nötig.
|
# Arcane läuft über seine eigene Schnittstelle, nicht über den Ausführer: kein Etikett nötig — nur für den
|
||||||
ziel.bausteine += _arcane_bausteine(url.rstrip("/") if url else None, zid)
|
# Snapshot vorher (Rückweg), den legt der Ausführer an.
|
||||||
|
ziel.bausteine += _arcane_bausteine(url.rstrip("/") if url else None, zid, gast)
|
||||||
return ziel
|
return ziel
|
||||||
if not gast.get("erlaubt"):
|
if not gast.get("erlaubt"):
|
||||||
ziel.bausteine.append(BausteinStand(
|
ziel.bausteine.append(BausteinStand(
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ def plan() -> dict:
|
|||||||
if not arcane.echt():
|
if not arcane.echt():
|
||||||
docker_probe = True # ein Probelauf ist kein Update
|
docker_probe = True # ein Probelauf ist kein Update
|
||||||
else:
|
else:
|
||||||
gaeste.append({**schritt, "rueckweg_art": "keiner"}) # Arcane legt keinen Snapshot an
|
gaeste.append(schritt) # Snapshot der VM nur mit Etikett watcher — rueckweg_art sagt es
|
||||||
elif b["id"] in GAST_BAUSTEINE:
|
elif b["id"] in GAST_BAUSTEINE:
|
||||||
gaeste.append(schritt)
|
gaeste.append(schritt)
|
||||||
gaeste.sort(key=_rang) # sort ist stabil: je Gast bleibt die Reihenfolge seiner Bausteine
|
gaeste.sort(key=_rang) # sort ist stabil: je Gast bleibt die Reihenfolge seiner Bausteine
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ Ablauf für einen Gast:
|
|||||||
4. Prüfen: läuft der Gast, antwortet die Weboberfläche, ist die App-Version jetzt neuer?
|
4. Prüfen: läuft der Gast, antwortet die Weboberfläche, ist die App-Version jetzt neuer?
|
||||||
5. Rot → zurück auf den Snapshot bzw. die Sicherung zurückspielen → dringende Meldung.
|
5. Rot → zurück auf den Snapshot bzw. die Sicherung zurückspielen → dringende Meldung.
|
||||||
Grün → ältere Snapshots bzw. Sicherungen des Orchestrators für diesen Gast weg → Meldung „eingespielt“.
|
Grün → ältere Snapshots bzw. Sicherungen des Orchestrators für diesen Gast weg → Meldung „eingespielt“.
|
||||||
|
Docker-Images (Arcane, VM 106) gehen über Arcanes eigenen Updater: bis zum Schalter „echt“ nur als Probelauf,
|
||||||
|
danach genauso mit Snapshot vorher und Rückweg bei Rot — sofern die VM freigegeben ist (Etikett watcher), sonst
|
||||||
|
ohne; Rückfrage und Update-Liste sagen es.
|
||||||
Jeder Lauf steht in <Datenordner>/homelab-laeufe.json und im strukturierten Update-Verlauf. Pro Ziel
|
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.
|
läuft höchstens ein Lauf; startet dieser Teil neu, gilt ein offener Lauf als unterbrochen.
|
||||||
|
|
||||||
@@ -42,6 +45,7 @@ WARTEN_NACH_UPDATE_S = 20
|
|||||||
ZEITLIMIT_UPDATE_S = 45 * 60
|
ZEITLIMIT_UPDATE_S = 45 * 60
|
||||||
ZEITLIMIT_KURZ_S = 15 * 60
|
ZEITLIMIT_KURZ_S = 15 * 60
|
||||||
ZEITLIMIT_SICHERUNG_S = 40 * 60 # Sicherung und Zurückspielen (der Ausführer gibt nach 30 min auf)
|
ZEITLIMIT_SICHERUNG_S = 40 * 60 # Sicherung und Zurückspielen (der Ausführer gibt nach 30 min auf)
|
||||||
|
ARCANE_FRIST_S = 180 # so lange darf Arcane nach einem Docker-Update brauchen, bis es wieder antwortet
|
||||||
BETREFF = "[Homelab-Update]"
|
BETREFF = "[Homelab-Update]"
|
||||||
BETREFF_ALARM = "[Alarm] Homelab-Update"
|
BETREFF_ALARM = "[Alarm] Homelab-Update"
|
||||||
SAMMELLAUF_SPERRE = "Es läuft gerade ‚Alle aktualisieren‘."
|
SAMMELLAUF_SPERRE = "Es läuft gerade ‚Alle aktualisieren‘."
|
||||||
@@ -243,28 +247,65 @@ def _aufraeumen(lauf: dict, vmid: int, rueckweg: tuple[str, str] | None) -> None
|
|||||||
lauf["schritte"].append(f"Alte Sicherung {volid} blieb stehen.")
|
lauf["schritte"].append(f"Alte Sicherung {volid} blieb stehen.")
|
||||||
|
|
||||||
|
|
||||||
def _docker_lauf(lauf: dict, basis: str) -> None:
|
def _antwortet_wieder(url: str) -> bool:
|
||||||
"""Docker über Arcanes Updater — zuerst nur als Probelauf (User-Entscheid 24.09.2026)."""
|
"""Nach dem Update starten Container neu: Arcane darf sich ARCANE_FRIST_S lang sammeln, bevor es rot heißt."""
|
||||||
|
ende = time.monotonic() + ARCANE_FRIST_S
|
||||||
|
while not inventar.erreichbar_frisch(url):
|
||||||
|
if time.monotonic() >= ende:
|
||||||
|
return False
|
||||||
|
time.sleep(10)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _docker_lauf(lauf: dict, basis: str, gast: dict) -> None:
|
||||||
|
"""Docker über Arcanes Updater — ein Probelauf, bis der Schalter in den Einstellungen auf „echt“ steht
|
||||||
|
(User-Entscheid 24.09.2026). Echt wie bei den Containern: vorher ein Snapshot der VM, wenn der Ausführer sie
|
||||||
|
anfassen darf (Etikett watcher); ist die Prüfung danach rot, geht es darauf zurück."""
|
||||||
probe = not arcane.echt()
|
probe = not arcane.echt()
|
||||||
|
rueckweg = None
|
||||||
|
if not probe and gast.get("erlaubt"):
|
||||||
|
try:
|
||||||
|
rueckweg = _rueckweg_anlegen(lauf, gast)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
_ende(lauf, "fehler", f"Arcane: Update nicht begonnen — {str(exc).rstrip('.')}. "
|
||||||
|
"Am Gerät wurde nichts geändert.")
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
ergebnisse = arcane.updater(basis, probelauf=probe)
|
ergebnisse = arcane.updater(basis, probelauf=probe)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
_ende(lauf, "fehler", f"Arcane: Updater gescheitert — {exc}", dringend=not probe)
|
if probe:
|
||||||
return
|
_ende(lauf, "fehler", f"Arcane: Probelauf gescheitert — {exc}")
|
||||||
|
return
|
||||||
|
ergebnisse, fehler = {}, [f"Der Updater brach ab ({exc})."]
|
||||||
|
else:
|
||||||
|
fehler = []
|
||||||
gesamt = {k: sum(int((e or {}).get(k) or 0) for e in ergebnisse.values()) for k in ("checked", "updated", "failed")}
|
gesamt = {k: sum(int((e or {}).get(k) or 0) for e in ergebnisse.values()) for k in ("checked", "updated", "failed")}
|
||||||
namen = [str(i.get("resourceName")) for e in ergebnisse.values() for i in (e or {}).get("items") or []
|
namen = [str(i.get("resourceName")) for e in ergebnisse.values() for i in (e or {}).get("items") or []
|
||||||
if isinstance(i, dict) and i.get("updateAvailable")]
|
if isinstance(i, dict) and i.get("updateAvailable")]
|
||||||
if probe:
|
if probe:
|
||||||
_ende(lauf, "offen", f"Arcane-Probelauf: {len(namen)} von {gesamt['checked']} Containern würden aktualisiert"
|
_ende(lauf, "offen", f"Arcane-Probelauf: {len(namen)} von {gesamt['checked']} Containern würden aktualisiert"
|
||||||
+ (f" ({', '.join(namen[:8])})" if namen else "") + ". Geändert wurde nichts.")
|
+ (f" ({', '.join(namen[:8])})" if namen else "") + ". Geändert wurde nichts.")
|
||||||
elif gesamt["failed"]:
|
return
|
||||||
_ende(lauf, "fehler", f"Arcane: {gesamt['failed']} Container ließen sich nicht aktualisieren, "
|
if gesamt["failed"]:
|
||||||
f"{gesamt['updated']} schon.", dringend=True)
|
fehler.append(f"{gesamt['failed']} Container ließen sich nicht aktualisieren, {gesamt['updated']} schon.")
|
||||||
else:
|
if not _antwortet_wieder(basis + "/"):
|
||||||
ok = inventar.erreichbar_frisch(basis + "/")
|
fehler.append("Arcane antwortet danach nicht.")
|
||||||
_ende(lauf, "eingespielt" if ok else "fehler",
|
if not fehler:
|
||||||
f"Arcane: {gesamt['updated']} Container aktualisiert" + ("." if ok else ", aber Arcane antwortet danach nicht."),
|
if rueckweg:
|
||||||
dringend=not ok)
|
_aufraeumen(lauf, gast["vmid"], rueckweg)
|
||||||
|
_ende(lauf, "eingespielt", f"Arcane: {gesamt['updated']} Container aktualisiert, Prüfung grün.")
|
||||||
|
return
|
||||||
|
if rueckweg:
|
||||||
|
try:
|
||||||
|
wie = _zurueck(lauf, gast["vmid"], rueckweg)
|
||||||
|
_ende(lauf, "zurueckgerollt", f"Arcane: Update gescheitert ({' '.join(fehler)}) — {wie}. "
|
||||||
|
"Läuft wieder wie vorher.", dringend=True)
|
||||||
|
return
|
||||||
|
except RuntimeError as exc:
|
||||||
|
fehler.append(f"Auch der Rückweg scheiterte: {exc}")
|
||||||
|
_ende(lauf, "fehler", f"Arcane: Update gescheitert — {' '.join(fehler)}"
|
||||||
|
+ ("" if rueckweg else " Es gab keinen Snapshot, also keinen automatischen Rückweg."),
|
||||||
|
dringend=True)
|
||||||
|
|
||||||
|
|
||||||
def _host_lauf(lauf: dict) -> None:
|
def _host_lauf(lauf: dict) -> None:
|
||||||
@@ -332,7 +373,7 @@ def starten(ziel_id: str, baustein: str, sammellauf: dict | None = None) -> dict
|
|||||||
if not (app and app.kennung == "arcane" and basis and arcane.schluessel()):
|
if not (app and app.kennung == "arcane" and basis and arcane.schluessel()):
|
||||||
return {"ok": False, "detail": "Docker-Updates gehen nur über Arcane mit API-Schlüssel."}
|
return {"ok": False, "detail": "Docker-Updates gehen nur über Arcane mit API-Schlüssel."}
|
||||||
lauf = _neuer_lauf(ziel_id, baustein, app.name, sammellauf)
|
lauf = _neuer_lauf(ziel_id, baustein, app.name, sammellauf)
|
||||||
threading.Thread(target=_docker_lauf, args=(lauf, basis), name=f"homelab-{ziel_id}",
|
threading.Thread(target=_docker_lauf, args=(lauf, basis, gast), name=f"homelab-{ziel_id}",
|
||||||
daemon=True).start()
|
daemon=True).start()
|
||||||
return {"ok": True, "lauf": lauf["id"]}
|
return {"ok": True, "lauf": lauf["id"]}
|
||||||
if not gast.get("erlaubt"):
|
if not gast.get("erlaubt"):
|
||||||
|
|||||||
@@ -284,6 +284,43 @@ def test_docker_ueber_arcane_erst_als_probelauf(ausfuehrer, monkeypatch):
|
|||||||
assert updates.starten("vm-106", "docker")["ok"] is False
|
assert updates.starten("vm-106", "docker")["ok"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_docker_echt_mit_snapshot_nur_mit_etikett(ausfuehrer, monkeypatch):
|
||||||
|
starten, protokoll, meldungen, _ = ausfuehrer
|
||||||
|
monkeypatch.setenv("MC_ARCANE_KEY", "schluessel")
|
||||||
|
monkeypatch.setenv("MC_ARCANE_ECHT", "1")
|
||||||
|
monkeypatch.setattr(updates, "ARCANE_FRIST_S", 0)
|
||||||
|
monkeypatch.setattr(inventar.arcane, "images_mit_update", lambda url: [
|
||||||
|
{"name": "nginx:alpine", "aktuell": None, "neu": None, "umgebung": 0, "benutzt": ["web"]}])
|
||||||
|
ergebnis = {"local": {"checked": 7, "updated": 1, "failed": 0, "items": []}}
|
||||||
|
monkeypatch.setattr(updates.arcane, "updater", lambda url, probelauf: ergebnis)
|
||||||
|
starten("1.27.3")
|
||||||
|
|
||||||
|
# Ohne Etikett (wie VM 106 heute): kein Snapshot — Update-Liste und Rückfrage sagen es, statt einen zu versprechen.
|
||||||
|
arcane_ziel = _ziele(None)["vm-106"]
|
||||||
|
docker = {b["id"]: b for b in arcane_ziel["bausteine"]}["docker"]
|
||||||
|
assert arcane_ziel["rueckweg_art"] == "keiner" and "Etikett „watcher“ fehlt" in docker["aktion"]["frage"]
|
||||||
|
lauf = _auf_ende(updates.starten("vm-106", "docker")["lauf"])
|
||||||
|
assert (lauf["ergebnis"], protokoll) == ("eingespielt", [])
|
||||||
|
|
||||||
|
# Mit Etikett: vorher ein Snapshot der VM; ist die Prüfung rot, geht es darauf zurück.
|
||||||
|
bericht = json.loads(json.dumps(BERICHT))
|
||||||
|
next(g for g in bericht["gaeste"] if g["vmid"] == 106)["erlaubt"] = True
|
||||||
|
kanal.bericht_speichern(bericht)
|
||||||
|
arcane_ziel = next(z for z in inventar.ziele()["ziele"] if z["id"] == "vm-106")
|
||||||
|
docker = {b["id"]: b for b in arcane_ziel["bausteine"]}["docker"]
|
||||||
|
assert arcane_ziel["rueckweg_art"] == "snapshot" and "Vorher wird ein Snapshot angelegt" in docker["aktion"]["frage"]
|
||||||
|
ergebnis["local"]["failed"] = 1
|
||||||
|
lauf = _auf_ende(updates.starten("vm-106", "docker")["lauf"])
|
||||||
|
assert (lauf["ergebnis"], protokoll) == ("zurueckgerollt", ["snapshot", "zurueck"])
|
||||||
|
text, dringend = meldungen[-1]
|
||||||
|
assert dringend and "zurück auf den Snapshot mc2-20260924-190000" in text and "1 Container" in text
|
||||||
|
|
||||||
|
ergebnis["local"]["failed"] = 0
|
||||||
|
lauf = _auf_ende(updates.starten("vm-106", "docker")["lauf"])
|
||||||
|
assert lauf["ergebnis"] == "eingespielt" and protokoll[2:] == ["snapshot"]
|
||||||
|
assert meldungen[-1] == ("Arcane: 1 Container aktualisiert, Prüfung grün.", False)
|
||||||
|
|
||||||
|
|
||||||
def test_ausfuehrer_nur_lesen(monkeypatch):
|
def test_ausfuehrer_nur_lesen(monkeypatch):
|
||||||
a = _ausfuehrer_modul()
|
a = _ausfuehrer_modul()
|
||||||
monkeypatch.setattr(a, "NUR_LESEN", True)
|
monkeypatch.setattr(a, "NUR_LESEN", True)
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ def test_richtiger_schluessel_wird_gespeichert_und_gilt_sofort(daten, arcane_nac
|
|||||||
assert datei.stat().st_mode & 0o077 == 0
|
assert datei.stat().st_mode & 0o077 == 0
|
||||||
assert _client().get("/api/homelab/einstellungen").json()["arcane"]["schluessel"] == "hinterlegt"
|
assert _client().get("/api/homelab/einstellungen").json()["arcane"]["schluessel"] == "hinterlegt"
|
||||||
# Ohne Neustart: Die Übersicht fragt Arcane mit dem neuen Schlüssel und sieht die Images.
|
# Ohne Neustart: Die Übersicht fragt Arcane mit dem neuen Schlüssel und sieht die Images.
|
||||||
_, docker = inventar._arcane_bausteine(url, "vm-106")
|
_, docker = inventar._arcane_bausteine(url, "vm-106", {})
|
||||||
assert (docker.zustand, docker.kurz, docker.aktion.label) == ("neu", "1 Images", "Probelauf")
|
assert (docker.zustand, docker.kurz, docker.aktion.label) == ("neu", "1 Images", "Probelauf")
|
||||||
assert RICHTIG not in caplog.text
|
assert RICHTIG not in caplog.text
|
||||||
|
|
||||||
@@ -180,7 +180,7 @@ def test_loeschen(daten):
|
|||||||
assert not (daten / "arcane.key").exists() and arcane.schluessel_quelle() == "fehlt"
|
assert not (daten / "arcane.key").exists() and arcane.schluessel_quelle() == "fehlt"
|
||||||
assert _client().delete("/api/homelab/einstellungen/arcane-schluessel").json()["text"] == (
|
assert _client().delete("/api/homelab/einstellungen/arcane-schluessel").json()["text"] == (
|
||||||
"Es war kein Arcane-Schlüssel hinterlegt.")
|
"Es war kein Arcane-Schlüssel hinterlegt.")
|
||||||
docker = inventar._arcane_bausteine("http://127.0.0.1:9", "vm-106")[1]
|
docker = inventar._arcane_bausteine("http://127.0.0.1:9", "vm-106", {})[1]
|
||||||
assert docker.zustand == "unbekannt" and docker.grund.startswith("Es fehlt ein Arcane-API-Schlüssel")
|
assert docker.zustand == "unbekannt" and docker.grund.startswith("Es fehlt ein Arcane-API-Schlüssel")
|
||||||
|
|
||||||
|
|
||||||
@@ -218,9 +218,9 @@ def test_echt_schalter(daten, monkeypatch):
|
|||||||
def test_echt_schalter_macht_aus_dem_probelauf_ein_update(daten, arcane_nachbau):
|
def test_echt_schalter_macht_aus_dem_probelauf_ein_update(daten, arcane_nachbau):
|
||||||
url, _ = arcane_nachbau
|
url, _ = arcane_nachbau
|
||||||
assert einstellungen.arcane_schluessel_setzen(RICHTIG)["ok"]
|
assert einstellungen.arcane_schluessel_setzen(RICHTIG)["ok"]
|
||||||
assert inventar._arcane_bausteine(url, "vm-106")[1].aktion.label == "Probelauf"
|
assert inventar._arcane_bausteine(url, "vm-106", {})[1].aktion.label == "Probelauf"
|
||||||
assert einstellungen.arcane_echt_setzen(True)["ok"]
|
assert einstellungen.arcane_echt_setzen(True)["ok"]
|
||||||
assert inventar._arcane_bausteine(url, "vm-106")[1].aktion.label == "Jetzt updaten"
|
assert inventar._arcane_bausteine(url, "vm-106", {})[1].aktion.label == "Jetzt updaten"
|
||||||
|
|
||||||
|
|
||||||
# --- Herkunftsprüfung (Middleware der App, Rolle homelab) ---------------------------------------------------------
|
# --- Herkunftsprüfung (Middleware der App, Rolle homelab) ---------------------------------------------------------
|
||||||
|
|||||||
+4
-1
@@ -326,7 +326,10 @@ flowchart LR
|
|||||||
(`arcane.key`); beides wird bei jedem Zugriff gelesen, ein neuer Schlüssel gilt ohne Neustart. Docker-Updates laufen
|
(`arcane.key`); beides wird bei jedem Zugriff gelesen, ein neuer Schlüssel gilt ohne Neustart. Docker-Updates laufen
|
||||||
zuerst nur als Probelauf (`dryRun`); echt, wenn der Schalter in den Einstellungen an ist. Setzt die Umgebung
|
zuerst nur als Probelauf (`dryRun`); echt, wenn der Schalter in den Einstellungen an ist. Setzt die Umgebung
|
||||||
`MC_ARCANE_ECHT`, gewinnt sie (`1` = echt). Die Arcane-VM braucht dafür kein Etikett, weil der Ausführer nicht
|
`MC_ARCANE_ECHT`, gewinnt sie (`1` = echt). Die Arcane-VM braucht dafür kein Etikett, weil der Ausführer nicht
|
||||||
beteiligt ist.
|
beteiligt ist — nur für den Rückweg: Mit Etikett `watcher` legt der Ausführer vor einem echten Docker-Update einen
|
||||||
|
Snapshot der VM an (`qm snapshot`), und ist die Prüfung danach rot (ein Container scheiterte, oder Arcane antwortet
|
||||||
|
nach 3 Minuten noch nicht), geht es darauf zurück. Ohne Etikett gibt es keinen Rückweg; Update-Liste, Rückfrage und
|
||||||
|
der Plan von „Alle aktualisieren“ sagen das (`rueckweg_art` = `keiner` für jeden Gast ohne Freigabe).
|
||||||
- **Wächter** in der Rolle `homelab`: Platte, Partner (die KI-Box), Ausführer (kein Bericht seit 30 min = rot),
|
- **Wächter** in der Rolle `homelab`: Platte, Partner (die KI-Box), Ausführer (kein Bericht seit 30 min = rot),
|
||||||
jede Weboberfläche der freigegebenen Gäste (außer mitten in ihrem Update-Lauf; das Ergebnis meldet der Lauf), die
|
jede Weboberfläche der freigegebenen Gäste (außer mitten in ihrem Update-Lauf; das Ergebnis meldet der Lauf), die
|
||||||
Platten der laufenden Container (ab 80 % gelb, ab 90 % rot — ext4 hält 5 % für root zurück; der Ausführer schickt `platte` mit), das
|
Platten der laufenden Container (ab 80 % gelb, ab 90 % rot — ext4 hält 5 % für root zurück; der Ausführer schickt `platte` mit), das
|
||||||
|
|||||||
+2
-1
@@ -155,7 +155,8 @@ Eine eigene Seite mit vier Karten: KI-Box, Homelab, Meldungen, Software-Stand.
|
|||||||
und speichern“ fragt Arcane zuerst, ob der Schlüssel gilt; erst dann wird er gespeichert — im Homelab-Teil, nicht
|
und speichern“ fragt Arcane zuerst, ob der Schlüssel gilt; erst dann wird er gespeichert — im Homelab-Teil, nicht
|
||||||
auf der KI-Box — und nie wieder angezeigt. Danach sieht der Orchestrator, welche Docker-Images neuer sind.
|
auf der KI-Box — und nie wieder angezeigt. Danach sieht der Orchestrator, welche Docker-Images neuer sind.
|
||||||
- **Docker-Updates (Homelab):** ab Werk nur ein Probelauf (Arcane zeigt, was es täte). „Echt schalten“ fragt nach und
|
- **Docker-Updates (Homelab):** ab Werk nur ein Probelauf (Arcane zeigt, was es täte). „Echt schalten“ fragt nach und
|
||||||
lässt sich jederzeit zurückschalten.
|
lässt sich jederzeit zurückschalten. Einen Snapshot der Arcane-VM vorher (und den Weg zurück bei Rot) gibt es nur,
|
||||||
|
wenn die VM in Proxmox das Etikett `watcher` trägt; sonst steht in der Update-Liste „Kein automatischer Rückweg“.
|
||||||
- **Ausführer und Updates (Homelab):** ob der Ausführer auf dem Proxmox-Host verbunden ist, in welchem Modus, wohin
|
- **Ausführer und Updates (Homelab):** ob der Ausführer auf dem Proxmox-Host verbunden ist, in welchem Modus, wohin
|
||||||
Sicherungen gehen und wie lange nach einer Änderung am Update-Skript einer App gewartet wird.
|
Sicherungen gehen und wie lange nach einer Änderung am Update-Skript einer App gewartet wird.
|
||||||
- **Hugging-Face-Zugang:** nötig für gesperrte Modelle und für schnellere Downloads. Eintragen, speichern, bei Bedarf
|
- **Hugging-Face-Zugang:** nötig für gesperrte Modelle und für schnellere Downloads. Eintragen, speichern, bei Bedarf
|
||||||
|
|||||||
Reference in New Issue
Block a user