homelab: Arcane und Docker (eigene Version oeffentlich, Images mit Schluessel, Updates zuerst als Probelauf); Datei-Sperren gegen Windows-Wettlauf
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5e9227c4a3
commit
209549f0f3
@@ -27,7 +27,7 @@ from kern.einstellungen import einstellungen
|
||||
from kern.zeit import LOCAL_TZ
|
||||
|
||||
from services import announce, update_verlauf
|
||||
from services.homelab import apps, inventar, kanal
|
||||
from services.homelab import apps, arcane, inventar, kanal
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -36,7 +36,7 @@ ZEITLIMIT_UPDATE_S = 45 * 60
|
||||
ZEITLIMIT_KURZ_S = 15 * 60
|
||||
BETREFF = "[Homelab-Update]"
|
||||
|
||||
_lock = threading.Lock()
|
||||
_lock = threading.RLock() # _merken liest und schreibt unter derselben Sperre
|
||||
|
||||
|
||||
def _pfad() -> Path:
|
||||
@@ -44,10 +44,11 @@ def _pfad() -> Path:
|
||||
|
||||
|
||||
def _laeufe() -> list[dict]:
|
||||
try:
|
||||
daten = json.loads(_pfad().read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
return []
|
||||
with _lock:
|
||||
try:
|
||||
daten = json.loads(_pfad().read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
return []
|
||||
return daten if isinstance(daten, list) else []
|
||||
|
||||
|
||||
@@ -164,6 +165,30 @@ def _aufraeumen(lauf: dict, vmid: int, behalten: str | None) -> None:
|
||||
lauf["schritte"].append(f"Alter Snapshot {name} blieb stehen.")
|
||||
|
||||
|
||||
def _docker_lauf(lauf: dict, basis: str) -> None:
|
||||
"""Docker über Arcanes Updater — zuerst nur als Probelauf (User-Entscheid 24.09.2026)."""
|
||||
probe = not arcane.echt()
|
||||
try:
|
||||
ergebnisse = arcane.updater(basis, probelauf=probe)
|
||||
except Exception as exc:
|
||||
_ende(lauf, "fehler", f"Arcane: Updater gescheitert — {exc}", dringend=not probe)
|
||||
return
|
||||
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 []
|
||||
if isinstance(i, dict) and i.get("updateAvailable")]
|
||||
if probe:
|
||||
_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.")
|
||||
elif gesamt["failed"]:
|
||||
_ende(lauf, "fehler", f"Arcane: {gesamt['failed']} Container ließen sich nicht aktualisieren, "
|
||||
f"{gesamt['updated']} schon.", dringend=True)
|
||||
else:
|
||||
ok = inventar.erreichbar_frisch(basis + "/")
|
||||
_ende(lauf, "eingespielt" if ok else "fehler",
|
||||
f"Arcane: {gesamt['updated']} Container aktualisiert" + ("." if ok else ", aber Arcane antwortet danach nicht."),
|
||||
dringend=not ok)
|
||||
|
||||
|
||||
def _host_lauf(lauf: dict) -> None:
|
||||
try:
|
||||
if lauf["baustein"] == "neustart":
|
||||
@@ -188,9 +213,16 @@ def _ende(lauf: dict, ergebnis: str, text: str, dringend: bool = False) -> None:
|
||||
_melden(text, dringend=dringend)
|
||||
|
||||
|
||||
def _neuer_lauf(ziel_id: str, baustein: str, name: str) -> dict:
|
||||
lauf = {"id": uuid.uuid4().hex[:12], "ziel": ziel_id, "baustein": baustein, "name": name, "status": "laeuft",
|
||||
"start": time.time(), "ende": None, "ergebnis": None, "text": None, "schritte": []}
|
||||
_merken(lauf)
|
||||
return lauf
|
||||
|
||||
|
||||
def starten(ziel_id: str, baustein: str) -> dict:
|
||||
"""Knopf „Jetzt updaten“. Rückgabe {"ok": True, "lauf": id} oder {"ok": False, "detail": …}."""
|
||||
if baustein not in ("app", "os", "pakete", "neustart"):
|
||||
if baustein not in ("app", "os", "pakete", "neustart", "docker"):
|
||||
return {"ok": False, "detail": "Unbekannter Baustein."}
|
||||
if any(x.get("ziel") == ziel_id and x.get("status") == "laeuft" for x in _laeufe()):
|
||||
return {"ok": False, "detail": "Für dieses Gerät läuft schon ein Update."}
|
||||
@@ -200,13 +232,19 @@ def starten(ziel_id: str, baustein: str) -> dict:
|
||||
gast = inventar.gast(ziel_id)
|
||||
if not gast:
|
||||
return {"ok": False, "detail": "Dieses Gerät steht nicht im Bericht des Ausführers."}
|
||||
if baustein == "docker":
|
||||
app = apps.app_fuer(None, gast.get("name"))
|
||||
basis = (apps.adresse(app, gast.get("ip")) or "").rstrip("/")
|
||||
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."}
|
||||
lauf = _neuer_lauf(ziel_id, baustein, app.name)
|
||||
threading.Thread(target=_docker_lauf, args=(lauf, basis), name=f"homelab-{ziel_id}", daemon=True).start()
|
||||
return {"ok": True, "lauf": lauf["id"]}
|
||||
if not gast.get("erlaubt"):
|
||||
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"))
|
||||
lauf = {"id": uuid.uuid4().hex[:12], "ziel": ziel_id, "baustein": baustein, "name": name, "status": "laeuft",
|
||||
"start": time.time(), "ende": None, "ergebnis": None, "text": None, "schritte": []}
|
||||
_merken(lauf)
|
||||
lauf = _neuer_lauf(ziel_id, baustein, name)
|
||||
ziel = _host_lauf if gast is None else (lambda lf: _gast_lauf(lf, gast))
|
||||
threading.Thread(target=ziel, args=(lauf,), name=f"homelab-{ziel_id}", daemon=True).start()
|
||||
return {"ok": True, "lauf": lauf["id"]}
|
||||
|
||||
Reference in New Issue
Block a user