Welle 3 · Wartungsfenster und Update-Regel je Gerät (So 03:00–04:15, Start: Sicherheitsupdates automatisch)

Der Steward des Homelab-Teils prüft jede Minute, ob das Fenster offen ist, und arbeitet dann einmal je Tag den Plan ab:
je Gerät nach seiner Regel — nur melden, Sicherheitsupdates automatisch (Standard, User-Entscheid) oder alles im
Fenster —, mit den bekannten Update-Läufen (Rückweg vorher, Prüfung danach, bei Rot zurück). Nach dem Fenster beginnt
kein Schritt mehr, Erfolge kommen als eine Meldung am Ende, Fehler sofort; der Proxmox-Host startet nie von selbst neu.
Der Ausführer meldet dafür die Namen der Sicherheitspakete und spielt auf Wunsch nur diese ein (Gäste und Host);
neuer Baustein „sicherheit“. Dazu „Version überspringen“ je App, die Einstellungen unter Einstellungen → Homelab und
„Wartung“ statt „Störung“ im Cockpit, solange das Fenster läuft.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-25 18:33:38 +02:00
co-authored by Claude Opus 5.5
parent 363bc211e1
commit 9da666d04e
18 changed files with 717 additions and 26 deletions
+24 -6
View File
@@ -42,7 +42,7 @@ from kern.einstellungen import einstellungen
from kern.zeit import LOCAL_TZ
from services import announce, update_verlauf
from services.homelab import apps, arcane, inventar, kanal, karenz
from services.homelab import apps, arcane, inventar, kanal, karenz, sicherheit
log = logging.getLogger(__name__)
@@ -202,7 +202,9 @@ def _gast_lauf(lauf: dict, gast: dict) -> None:
"Am Gerät wurde nichts geändert.")
return
try:
_auftrag(lauf, "update" if baustein == "app" else "os_update", {"vmid": vmid}, ZEITLIMIT_UPDATE_S)
# Seit 25.09.2026 „sicherheit“: nur die Pakete, die eine Lücke schließen (Wartungsfenster, Lucy).
mehr = {"pakete": lauf["sicherheit_pakete"]} if baustein == "sicherheit" else {}
_auftrag(lauf, "update" if baustein == "app" else "os_update", {"vmid": vmid, **mehr}, ZEITLIMIT_UPDATE_S)
time.sleep(WARTEN_NACH_UPDATE_S)
nachher = _frischer_gast(lauf, vmid)
if baustein == "app" and nachher:
@@ -366,9 +368,11 @@ def _host_lauf(lauf: dict) -> None:
_auftrag(lauf, "host_neustart", {}, 60)
_ende(lauf, "neustart", "Der Proxmox-Host startet neu. Die KI-Box meldet, ob alles wiederkommt.")
return
_auftrag(lauf, "host_update", {}, 60 * 60)
sicherheit = lauf["baustein"] == "sicherheit"
_auftrag(lauf, "host_update", {"pakete": lauf["sicherheit_pakete"]} if sicherheit else {}, 60 * 60)
_auftrag(lauf, "bericht", {}, 5 * 60)
_ende(lauf, "eingespielt", "Proxmox-Host: Pakete eingespielt. Ein Neustart ist ein eigener Knopf.")
_ende(lauf, "eingespielt", f"Proxmox-Host: {'Sicherheitsupdates' if sicherheit else 'Pakete'} eingespielt. "
"Ein Neustart ist ein eigener Knopf.")
except RuntimeError as exc:
was = "Kernel aufräumen" if lauf["baustein"] == "kernel" else "Update"
_ende(lauf, "fehler", f"Proxmox-Host: {was} gescheitert — {exc}", dringend=True)
@@ -385,7 +389,7 @@ def _ende(lauf: dict, ergebnis: str, text: str, dringend: bool = False) -> None:
try:
# Die Schritte eines Sammellaufs stehen im Update-Verlauf als EIN Lauf (gleicher Beginn, eigener Anlass).
start = datetime.fromtimestamp(lauf.get("sammellauf_start") or lauf["start"], LOCAL_TZ)
anlass = "Alle aktualisieren" if lauf.get("sammellauf") else "Update von Hand"
anlass = lauf.get("anlass") or ("Alle aktualisieren" if lauf.get("sammellauf") else "Update von Hand")
update_verlauf.eintragen(start.isoformat(timespec="seconds"), anlass, lauf["ziel"], ergebnis, text)
except Exception:
log.warning("homelab: Verlauf nicht geschrieben", exc_info=True)
@@ -398,6 +402,8 @@ def _neuer_lauf(ziel_id: str, baustein: str, name: str, sammellauf: dict | None
"start": time.time(), "ende": None, "ergebnis": None, "text": None, "schritte": [], **mehr}
if sammellauf:
lauf.update(sammellauf=sammellauf["id"], sammellauf_start=sammellauf["start"])
if sammellauf.get("anlass"): # „Wartungsfenster“ (seit 25.09.2026)
lauf["anlass"] = sammellauf["anlass"]
_merken(lauf)
return lauf
@@ -407,7 +413,7 @@ def starten(ziel_id: str, baustein: str, sammellauf: dict | None = None) -> dict
sammellauf ({"id", "start"}) setzt nur „Alle aktualisieren“ (sammellauf.py) für seine Schritte; ohne ihn lehnt
der Knopf ab, solange ein Sammellauf läuft."""
if baustein not in ("app", "os", "pakete", "neustart", "docker", "kernel", "probe"):
if baustein not in ("app", "os", "pakete", "neustart", "docker", "kernel", "probe", "sicherheit"):
return {"ok": False, "detail": "Unbekannter Baustein."}
with START_SPERRE:
if sammellauf is None and _sammellauf_laeuft():
@@ -421,6 +427,11 @@ def starten(ziel_id: str, baustein: str, sammellauf: dict | None = None) -> dict
host = ((kanal.bericht() or {}).get("bericht") or {}).get("host") or {}
mehr = ({"pakete": len(host.get("updates") or [])} if baustein == "pakete"
else {"kernel": len(host.get("kernel_alt") or [])} if baustein == "kernel" else {})
if baustein == "sicherheit":
namen = [str(n) for n in host.get("updates_sicherheit") or []]
if not namen:
return {"ok": False, "detail": "Auf dem Proxmox-Host steht kein Sicherheitsupdate an."}
mehr = {"pakete": len(namen), "sicherheit_pakete": namen}
else:
gast = inventar.gast(ziel_id)
if not gast:
@@ -453,6 +464,13 @@ def starten(ziel_id: str, baustein: str, sammellauf: dict | None = None) -> dict
# Was vorher war, fürs Protokoll: die App-Version bzw. die Zahl der Pakete.
mehr = ({"version_alt": (gast.get("app") or {}).get("version")} if baustein == "app"
else {"pakete": (gast.get("os_updates") or {}).get("anzahl")})
if baustein == "sicherheit":
os_stand = gast.get("os_updates") or {}
namen = os_stand.get("sicherheit_pakete") if "sicherheit_pakete" in os_stand \
else sicherheit.alpine_pakete(os_stand)
if not namen:
return {"ok": False, "detail": f"Bei {name} steht kein Sicherheitsupdate an."}
mehr = {"pakete": len(namen), "sicherheit_pakete": [str(n) for n in namen]}
lauf = _neuer_lauf(ziel_id, baustein, name, sammellauf, **mehr)
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()