homelab: Images ohne Container sind kein Docker-Update; neuer Kernel heißt Neustart

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 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-25 11:46:26 +02:00
co-authored by Claude Opus 5.5
parent 903fbbfbb9
commit 17d4ee3ea8
3 changed files with 57 additions and 0 deletions
+10
View File
@@ -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.",
+30
View File
@@ -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"
+17
View File
@@ -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}