Der Ausführer zählt je Gast, wie viele offene Updates aus einer Sicherheits-Suite kommen („-security“ in apt list), beim Proxmox-Host ebenso; für Alpine (NPMplus) schickt er die Paketliste, und der Homelab-Teil gleicht sie mit Alpines Sicherheitsdatenbank ab (im Hintergrund geladen, zwölf Stunden gemerkt, Alpine-Versionsvergleich). Die Update-Tabelle, die Kacheln und die Übersicht zeigen „· 3 Sicherheit“, die Rückfrage nennt es. Je Docker-Container steht, was Arcanes Lücken-Scan (Trivy, sonntags 02:00) im Image fand, kritische und hohe. Der Ausführer braucht ausfuehrer-einrichten.sh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
97 lines
5.2 KiB
Python
97 lines
5.2 KiB
Python
"""Docker-Dienste unter Arcane einzeln (Ausbauplan Welle 2, Punkt 8, seit 25.09.2026).
|
|
|
|
Bis zum 25.09. stand im Orchestrator nur „Arcane (Docker) · 11 Images“. Jetzt fragt er Arcane nach jedem Container
|
|
(GET /environments/{id}/containers, mit dem Schlüssel aus den Einstellungen, höchstens alle zwei Minuten) und zeigt je
|
|
Stack (Compose-Projekt) Zustand, Gesundheit, Image und Update-Stand:
|
|
• „Update verfügbar“, wenn Arcane für das Image einen neueren Stand kennt,
|
|
• „lokal gebaut“, wenn das Image aus keiner Registry kommt (Arcane: updateType „local“, oder die Registry kennt es
|
|
nicht — NerdQuiz und Rippy baut der User selbst) — dort gibt es keinen Abgleich,
|
|
• sonst „aktuell“.
|
|
Seit 25.09.2026 (Punkt 11) steht je Container auch, was Arcanes Lücken-Scan (Trivy, sonntags 02:00) im Image gefunden
|
|
hat — kritische und hohe zuerst; ungescannte Images haben keine Angabe.
|
|
Der Wächter meldet gelb, wenn ein Container ungesund ist (Healthcheck) oder immer wieder neu startet; beendete
|
|
Container (etwa einmalige Aufgaben) sind kein Befund. Neue Stacks erscheinen unter „Neu entdeckt“ (entdecken.py).
|
|
"""
|
|
|
|
import logging
|
|
import re
|
|
|
|
from services.homelab import apps, arcane, einstellungen, kanal
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
_GESUND = re.compile(r"\((healthy|unhealthy|health: starting)\)")
|
|
_LOKAL = re.compile(r"distribution inspect|manifest unknown|pull access denied|repository does not exist", re.IGNORECASE)
|
|
|
|
|
|
def container_zeile(c: dict) -> dict:
|
|
"""Ein Container aus Arcanes Liste in der Form der Oberfläche."""
|
|
labels = c.get("labels") if isinstance(c.get("labels"), dict) else {}
|
|
info = c.get("updateInfo") if isinstance(c.get("updateInfo"), dict) else {}
|
|
gesund = _GESUND.search(str(c.get("status") or ""))
|
|
fehler = str(info.get("error") or "")
|
|
if info.get("hasUpdate"):
|
|
update = "neu"
|
|
elif info.get("updateType") == "local" or _LOKAL.search(fehler):
|
|
update = "lokal"
|
|
elif fehler:
|
|
update = "unbekannt"
|
|
else:
|
|
update = "aktuell"
|
|
name = str((c.get("names") or ["?"])[0]).lstrip("/")
|
|
return {"id": str(c.get("id") or name)[:12], "image_id": c.get("imageId"), "luecken": None,
|
|
"name": name, "stack": labels.get("com.docker.compose.project"),
|
|
"dienst": labels.get("com.docker.compose.service"), "image": c.get("image"),
|
|
"zustand": str(c.get("state") or "unbekannt"), "status": c.get("status"),
|
|
"gesund": {"healthy": True, "unhealthy": False}.get(gesund.group(1)) if gesund else None,
|
|
"update": update, "aktuell": info.get("currentVersion") or None, "neu": info.get("latestVersion") or None}
|
|
|
|
|
|
def lage() -> dict:
|
|
"""Für GET /api/homelab/docker: {"verfuegbar", "grund", "stacks": [{"name", "container": […]}]}."""
|
|
url = einstellungen.arcane_url()
|
|
if not url:
|
|
return {"verfuegbar": False, "grund": "Arcane steht nicht im Bericht des Ausführers.", "stacks": []}
|
|
if not arcane.schluessel():
|
|
return {"verfuegbar": False, "grund": "Es fehlt der Arcane-API-Schlüssel (Einstellungen → Homelab).", "stacks": []}
|
|
roh = arcane.container_liste(url)
|
|
if roh is None:
|
|
return {"verfuegbar": False, "grund": "Arcane antwortet gerade nicht.", "stacks": []}
|
|
stacks: dict[str, list[dict]] = {}
|
|
zeilen = [container_zeile(c) for c in roh]
|
|
befunde = arcane.luecken(url, tuple(sorted({z["image_id"] for z in zeilen if z["image_id"]})))
|
|
for zeile in zeilen:
|
|
zeile["luecken"] = befunde.get(zeile["image_id"] or "")
|
|
stacks.setdefault(zeile["stack"] or zeile["name"], []).append(zeile)
|
|
return {"verfuegbar": True, "grund": None,
|
|
"stacks": [{"name": n, "container": sorted(cs, key=lambda z: z["name"])} for n, cs in sorted(stacks.items())]}
|
|
|
|
|
|
def arcane_ziel() -> str:
|
|
"""Die Ziel-ID der Arcane-VM (für die Hinweise), aus dem Bericht; sonst „arcane“."""
|
|
for g in ((kanal.bericht() or {}).get("bericht") or {}).get("gaeste") or []:
|
|
app = apps.app_fuer((g.get("app") or {}).get("kennung"), g.get("name"))
|
|
if app and app.kennung == "arcane":
|
|
return f"{'ct' if g.get('art') == 'lxc' else 'vm'}-{g['vmid']}"
|
|
return "arcane"
|
|
|
|
|
|
def pruefe_docker() -> list:
|
|
"""Wächter: gelb, wenn ein Container ungesund ist oder ständig neu startet. Ohne Arcane oder Schlüssel: nichts."""
|
|
from services.waechter import Befund # der Wächter registriert diese Prüfung, also erst hier
|
|
befunde = []
|
|
quelle = None
|
|
for s in lage()["stacks"]:
|
|
for c in s["container"]:
|
|
if c["gesund"] is False:
|
|
titel, text = (f"Docker-Dienst {c['name']} ist ungesund",
|
|
f"Sein Healthcheck schlägt fehl ({c['status']}). In Arcane: Container → {c['name']} → Logs.")
|
|
elif c["zustand"] == "restarting":
|
|
titel, text = (f"Docker-Dienst {c['name']} startet immer wieder neu",
|
|
f"Er kommt nicht hoch ({c['status']}). In Arcane: Container → {c['name']} → Logs.")
|
|
else:
|
|
continue
|
|
quelle = quelle or arcane_ziel()
|
|
befunde.append(Befund(id=f"docker:{c['name']}", stufe="gelb", titel=titel, text=text, quelle=quelle))
|
|
return befunde
|