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
@@ -0,0 +1,99 @@
|
||||
"""Arcane und Docker auf der Arcane-VM (Phase 3/4, 24.09.2026).
|
||||
|
||||
Arcane (getarcaneapp/arcane) verwaltet die Docker-Container der VM (NerdQuiz, Rippy …) und hat eine
|
||||
eigene Schnittstelle (OpenAPI unter /api/openapi.json). Öffentlich ist nur die eigene Version
|
||||
(/api/app-version mit currentVersion, newestVersion, updateAvailable). Alles über Images braucht einen
|
||||
API-Schlüssel (Kopfzeile X-API-Key), den man in Arcane anlegt — MC_ARCANE_KEY in /etc/mc2/homelab.env.
|
||||
|
||||
Docker-Updates laufen über Arcanes eigenen Updater (POST /environments/{id}/updater/run). User-Entscheid
|
||||
24.09.: zuerst nur als Probelauf (dryRun); echte Updates erst mit MC_ARCANE_ECHT=1.
|
||||
"""
|
||||
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
import httpx
|
||||
|
||||
ZEITLIMIT = httpx.Timeout(20.0, connect=5.0)
|
||||
CACHE_S = 120
|
||||
_cache: dict[str, tuple[float, object]] = {}
|
||||
_lock = threading.Lock()
|
||||
|
||||
|
||||
def _gemerkt(schluessel: str, holen):
|
||||
"""Arcane merkt sich den Update-Stand selbst; die Übersicht fragt ihn höchstens alle zwei Minuten ab."""
|
||||
jetzt = time.time()
|
||||
with _lock:
|
||||
if (eintrag := _cache.get(schluessel)) and jetzt - eintrag[0] < CACHE_S:
|
||||
return eintrag[1]
|
||||
wert = holen()
|
||||
with _lock:
|
||||
_cache[schluessel] = (jetzt, wert)
|
||||
return wert
|
||||
|
||||
|
||||
def schluessel() -> str:
|
||||
return os.environ.get("MC_ARCANE_KEY", "").strip()
|
||||
|
||||
|
||||
def echt() -> bool:
|
||||
return os.environ.get("MC_ARCANE_ECHT", "") == "1"
|
||||
|
||||
|
||||
def app_version(url: str) -> dict | None:
|
||||
"""Arcanes eigene Version (öffentlich): {"installiert", "neu", "update"} oder None."""
|
||||
return _gemerkt(f"version:{url}", lambda: _app_version(url))
|
||||
|
||||
|
||||
def _app_version(url: str) -> dict | None:
|
||||
try:
|
||||
d = httpx.get(f"{url}/api/app-version", timeout=ZEITLIMIT).json()
|
||||
except (httpx.HTTPError, ValueError):
|
||||
return None
|
||||
if not isinstance(d, dict) or not d.get("currentVersion"):
|
||||
return None
|
||||
return {"installiert": str(d["currentVersion"]).lstrip("v"), "neu": str(d.get("newestVersion") or "").lstrip("v") or None,
|
||||
"update": bool(d.get("updateAvailable"))}
|
||||
|
||||
|
||||
def _get(url: str, pfad: str) -> object:
|
||||
r = httpx.get(f"{url}/api{pfad}", headers={"X-API-Key": schluessel()}, timeout=ZEITLIMIT)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def umgebungen(url: str) -> list[dict]:
|
||||
daten = _get(url, "/environments?limit=50")
|
||||
return [e for e in (daten or {}).get("data") or [] if isinstance(e, dict) and e.get("id") is not None]
|
||||
|
||||
|
||||
def images_mit_update(url: str) -> list[dict]:
|
||||
"""Images mit einem neueren Stand in der Registry, über alle Umgebungen: {"name", "aktuell", "neu", "benutzt"}."""
|
||||
return _gemerkt(f"images:{url}", lambda: _images_mit_update(url))
|
||||
|
||||
|
||||
def _images_mit_update(url: str) -> list[dict]:
|
||||
ergebnis = []
|
||||
for umgebung in umgebungen(url):
|
||||
daten = _get(url, f"/environments/{umgebung['id']}/images?limit=500")
|
||||
for image in (daten or {}).get("data") or []:
|
||||
info = image.get("updateInfo") or {}
|
||||
if not info.get("hasUpdate"):
|
||||
continue
|
||||
name = f"{image.get('repo') or '?'}:{image.get('tag') or 'latest'}"
|
||||
ergebnis.append({"name": name, "aktuell": info.get("currentVersion") or None,
|
||||
"neu": info.get("latestVersion") or None, "umgebung": umgebung["id"],
|
||||
"benutzt": [str(u.get("name") if isinstance(u, dict) else u) for u in image.get("usedBy") or []]})
|
||||
return ergebnis
|
||||
|
||||
|
||||
def updater(url: str, probelauf: bool) -> dict:
|
||||
"""Arcanes Updater für alle Umgebungen. Rückgabe: je Umgebung das Ergebnis (geprüft, aktualisiert, Fehler)."""
|
||||
ergebnisse = {}
|
||||
for umgebung in umgebungen(url):
|
||||
r = httpx.post(f"{url}/api/environments/{umgebung['id']}/updater/run", headers={"X-API-Key": schluessel()},
|
||||
json={"dryRun": probelauf}, timeout=httpx.Timeout(600.0, connect=5.0))
|
||||
r.raise_for_status()
|
||||
ergebnisse[str(umgebung.get("name") or umgebung["id"])] = (r.json() or {}).get("data") or {}
|
||||
return ergebnisse
|
||||
Reference in New Issue
Block a user