From 17d4ee3ea86db5144b2e03a471d67e155573dcbc Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Fri, 25 Sep 2026 11:46:26 +0200 Subject: [PATCH] =?UTF-8?q?homelab:=20Images=20ohne=20Container=20sind=20k?= =?UTF-8?q?ein=20Docker-Update;=20neuer=20Kernel=20hei=C3=9Ft=20Neustart?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nach dem ersten echten „Alle aktualisieren“ (25.09., 8 Schritte grün): - 9 veraltete Images, die kein Container benutzt (Basis-Images für Builds/CI), blieben für immer „neu“ – Arcanes Updater tauscht nur Container. Sie zählen jetzt nicht mehr, der Grund nennt sie als Aufräum-Hinweis. - Proxmox legt bei Kernel-Updates kein /var/run/reboot-required an: 7.0.14-19 war installiert, 7.0.14-12 lief, kein Neustart-Knopf. Der Ausführer vergleicht jetzt den neuesten installierten (oder festgepinnten) Kernel mit dem laufenden. Co-Authored-By: Claude Opus 5.5 --- backend/services/homelab/inventar.py | 10 ++++++++++ backend/tests/test_homelab.py | 30 ++++++++++++++++++++++++++++ deploy/homelab/ausfuehrer.py | 17 ++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/backend/services/homelab/inventar.py b/backend/services/homelab/inventar.py index 4163fa1..46061dc 100644 --- a/backend/services/homelab/inventar.py +++ b/backend/services/homelab/inventar.py @@ -196,6 +196,13 @@ def _arcane_bausteine(basis: str | None, zid: str, gast: dict) -> list[BausteinS except Exception: docker.grund = "Arcane verweigert die Auskunft oder antwortet nicht (Schlüssel prüfen)." else: + # Nur Images, die ein Container benutzt: Arcanes Updater tauscht Container aus. Ein veraltetes Image ohne + # Container (Basis-Images für Builds, CI) bliebe sonst für immer „neu“ (25.09.2026: 9 von 11 nach dem Lauf). + ohne = [i for i in images if not i.get("benutzt")] + images = [i for i in images if i.get("benutzt")] + if ohne: + docker.grund = (f"{len(ohne)} ältere Images ohne Container ({', '.join(i['name'] for i in ohne[:4])}" + + (" …" if len(ohne) > 4 else "") + ") betreffen nichts; aufräumen in Arcane unter Images.") if images: docker.zustand, docker.kurz = "neu", f"{len(images)} Images" docker.grund = ", ".join(i["name"] for i in images[:6]) + (" …" if len(images) > 6 else "") @@ -259,6 +266,9 @@ def _host_ziel(host: dict) -> ZielStand: if host.get("neustart_noetig"): ziel.bausteine.append(BausteinStand( id="neustart", name="Neustart", zustand="neu", kurz="steht an", + installiert=host.get("kernel"), verfuegbar=host.get("kernel_neu"), + grund=(f"Neuer Kernel {host['kernel_neu']} ist installiert, es läuft noch {host.get('kernel')}." + if host.get("kernel_neu") else "Ein Update verlangt einen Neustart."), aktion=Aktion("POST", "/api/homelab/ziele/pve/neustart", "Den Proxmox-Host jetzt neu starten? ALLE Container und die Arcane-VM sind dann ein paar " "Minuten weg. Die KI-Box prüft danach von außen, ob alles wiederkommt.", diff --git a/backend/tests/test_homelab.py b/backend/tests/test_homelab.py index b94704d..ab03361 100644 --- a/backend/tests/test_homelab.py +++ b/backend/tests/test_homelab.py @@ -421,3 +421,33 @@ def test_waechter_und_uebersicht_sehen_volle_gastplatten(daten): z = {z["id"]: z for z in inventar.ziele()["ziele"]} assert z["ct-100"]["platte"] == {"belegt": 2_000_000_000, "gesamt": 2_040_000_000} and z["pve"]["platte"] is None + + +def test_images_ohne_container_sind_kein_update(daten, monkeypatch): + """25.09.2026: Nach dem ersten echten Lauf blieben 9 Images „neu“ — Basis-Images, die kein Container benutzt.""" + monkeypatch.setenv("MC_ARCANE_KEY", "schluessel") + ohne = [{"name": n, "aktuell": None, "neu": None, "umgebung": 0, "benutzt": []} for n in ("alpine:latest", "node:22")] + monkeypatch.setattr(inventar.arcane, "images_mit_update", lambda url: ohne) + docker = {b["id"]: b for b in _ziele(None)["vm-106"]["bausteine"]}["docker"] + assert (docker["zustand"], docker["aktion"]) == ("aktuell", None) + assert docker["grund"].startswith("2 ältere Images ohne Container (alpine:latest, node:22)") + genutzt = {"name": "ghcr.io/tobi/nerdquiz:latest", "aktuell": None, "neu": None, "umgebung": 0, "benutzt": ["quiz"]} + monkeypatch.setattr(inventar.arcane, "images_mit_update", lambda url: [*ohne, genutzt]) + docker = {b["id"]: b for b in _ziele(None)["vm-106"]["bausteine"]}["docker"] + assert (docker["zustand"], docker["kurz"]) == ("neu", "1 Images") + + +def test_neuer_kernel_heisst_neustart(daten): + """25.09.2026: Proxmox legt bei Kernel-Updates kein reboot-required an — der Ausführer vergleicht selbst.""" + a = _ausfuehrer_modul() + vorhanden = ["6.8.12-4-pve", "7.0.14-12-pve", "7.0.14-19-pve"] + assert a.neuer_kernel("7.0.14-12-pve", vorhanden) == "7.0.14-19-pve" + assert a.neuer_kernel("7.0.14-19-pve", vorhanden) is None + assert a.neuer_kernel("7.0.14-12-pve", vorhanden, pin="7.0.14-12-pve") is None # festgepinnt + assert a.neuer_kernel(None, vorhanden) is None and a.neuer_kernel("7.0.14-12-pve", []) is None + bericht = json.loads(json.dumps(BERICHT)) + bericht["host"].update(neustart_noetig=True, kernel="7.0.14-12-pve", kernel_neu="7.0.14-19-pve") + kanal.bericht_speichern(bericht) + host = {b["id"]: b for b in next(z for z in inventar.ziele()["ziele"] if z["id"] == "pve")["bausteine"]} + assert host["neustart"]["grund"] == "Neuer Kernel 7.0.14-19-pve ist installiert, es läuft noch 7.0.14-12-pve." + assert host["neustart"]["aktion"]["label"] == "Jetzt neu starten" diff --git a/deploy/homelab/ausfuehrer.py b/deploy/homelab/ausfuehrer.py index 41e23d5..43bac7b 100644 --- a/deploy/homelab/ausfuehrer.py +++ b/deploy/homelab/ausfuehrer.py @@ -637,9 +637,26 @@ def _host() -> dict: host["fehler"] = str(exc)[:200] code, kernel = _laufen(["uname", "-r"], 5) host["kernel"] = kernel if code == 0 else None + vorhanden = [os.path.basename(p)[len("vmlinuz-"):] for p in glob.glob("/boot/vmlinuz-*")] + host["kernel_neu"] = neuer_kernel(host["kernel"], vorhanden, _lesen("/etc/kernel/proxmox-boot-pin").strip() or None) + host["neustart_noetig"] = host["neustart_noetig"] or bool(host["kernel_neu"]) return host +def _versionsschluessel(version: str) -> tuple[int, ...]: + return tuple(int(x) for x in re.findall(r"\d+", version)) + + +def neuer_kernel(laufend: str | None, vorhanden: list[str], pin: str | None = None) -> str | None: + """Der Kernel, mit dem der Host beim nächsten Start hochkäme, wenn es nicht der laufende ist (ein festgepinnter + geht vor). Proxmox legt bei Kernel-Updates keine /var/run/reboot-required an — am 25.09.2026 war 7.0.14-19 + installiert, 7.0.14-12 lief, und nichts zeigte einen Neustart an.""" + if not laufend or not vorhanden: + return None + naechster = pin if pin in vorhanden else max(vorhanden, key=_versionsschluessel) + return None if naechster == laufend else naechster + + def bericht() -> dict: speicher = _speicher_liste() arten = {s["storage"]: s.get("type") for s in speicher}