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:
co-authored by
Claude Opus 5.5
parent
363bc211e1
commit
9da666d04e
@@ -323,12 +323,16 @@ def _app_version(vmid: int, kennung: str) -> str | None:
|
||||
# Sicherheitsupdates (seit 25.09.2026, Ausbauplan Welle 2, Punkt 11): Debian und Ubuntu liefern sie aus eigenen Suiten
|
||||
# („trixie-security“, „jammy-security“) — das steht in `apt list --upgradable` hinter dem Paketnamen. Alpine hat das
|
||||
# nicht; dort geht die Liste mit den Versionen mit, und der Homelab-Teil gleicht sie mit Alpines Sicherheitsdatenbank ab.
|
||||
# Zeile 1: alle offenen Pakete, Zeile 2: die Namen der Sicherheitsupdates (Wartungsfenster: nur diese einspielen),
|
||||
# Zeile 3: Stand der Paketlisten.
|
||||
APT_ZAEHLEN = ("LC_ALL=C apt list --upgradable 2>/dev/null"
|
||||
" | awk '/upgradable/{n++; if ($1 ~ /-security/) s++} END{print n+0; print s+0}'; "
|
||||
" | awk '/upgradable/{n++; if ($1 ~ /-security/) {split($1, t, \"/\"); s = s t[1] \" \"}}"
|
||||
" END{print n+0; print s}'; "
|
||||
"stat -c %Y /var/lib/apt/lists 2>/dev/null || echo 0")
|
||||
APK_LISTE = ("cat /etc/alpine-release 2>/dev/null || echo ?; stat -c %Y /var/cache/apk 2>/dev/null || echo 0; "
|
||||
"apk version -l '<' 2>/dev/null | tail -n +2")
|
||||
_APK_ZEILE = re.compile(r"^(\S+?)-(\d\S*)\s+<\s+(\S+)$")
|
||||
_PAKETNAME = re.compile(r"[a-z0-9][a-z0-9+.-]{1,80}")
|
||||
|
||||
|
||||
def apk_auswerten(text: str) -> dict:
|
||||
@@ -351,9 +355,11 @@ def _os_updates(vmid: int, ostype: str) -> dict:
|
||||
code, text = _im_gast(vmid, APK_LISTE, 30)
|
||||
return apk_auswerten(text) if code in (0, 1) else {"anzahl": None, "listen_stand": None, "fehler": text[:160]}
|
||||
code, text = _im_gast(vmid, APT_ZAEHLEN, 30)
|
||||
zeilen = text.split() if code in (0, 1) else []
|
||||
zeilen = text.splitlines() if code in (0, 1) else []
|
||||
try:
|
||||
return {"anzahl": int(zeilen[0]), "sicherheit": int(zeilen[1]), "listen_stand": int(zeilen[2]) or None}
|
||||
namen = [n for n in zeilen[1].split() if _PAKETNAME.fullmatch(n)]
|
||||
return {"anzahl": int(zeilen[0]), "sicherheit": len(namen), "sicherheit_pakete": namen[:200],
|
||||
"listen_stand": int(zeilen[2]) or None}
|
||||
except (IndexError, ValueError):
|
||||
return {"anzahl": None, "listen_stand": None, "fehler": text[:160]}
|
||||
|
||||
@@ -1150,13 +1156,32 @@ def a_suchen(p: dict) -> tuple[int, str]:
|
||||
return _im_gast(vmid, "if command -v apk >/dev/null; then apk update; else apt-get update -q; fi", 300)
|
||||
|
||||
|
||||
def _paketliste(p: dict) -> list[str] | None:
|
||||
"""Die Paketliste eines Auftrags (seit 25.09.2026: nur diese einspielen, etwa die Sicherheitsupdates im
|
||||
Wartungsfenster) — None, wenn der Auftrag keine nennt. Jeder Name wird geprüft; er landet in einer Befehlszeile."""
|
||||
pakete = p.get("pakete")
|
||||
if pakete is None:
|
||||
return None
|
||||
if not isinstance(pakete, list) or not 1 <= len(pakete) <= 300 \
|
||||
or not all(isinstance(x, str) and _PAKETNAME.fullmatch(x) for x in pakete):
|
||||
raise ValueError("ungültige Paketliste")
|
||||
return pakete
|
||||
|
||||
|
||||
def a_os_update(p: dict) -> tuple[int, str]:
|
||||
"""Die Pakete des Gasts (unabhängig vom App-Update des Community-Scripts)."""
|
||||
"""Die Pakete des Gasts (unabhängig vom App-Update des Community-Scripts); mit „pakete“ nur diese."""
|
||||
art, vmid = _gast_pruefen(p.get("vmid"))
|
||||
if art != "lxc":
|
||||
raise ValueError("nur für Container")
|
||||
befehl = ("if command -v apk >/dev/null; then apk update && apk upgrade; else apt-get update -q && "
|
||||
"DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::=--force-confold dist-upgrade; fi")
|
||||
pakete = _paketliste(p)
|
||||
if pakete:
|
||||
namen = " ".join(pakete)
|
||||
befehl = (f"if command -v apk >/dev/null; then apk update && apk upgrade {namen}; else apt-get update -q && "
|
||||
"DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::=--force-confold install "
|
||||
f"--only-upgrade {namen}; fi")
|
||||
else:
|
||||
befehl = ("if command -v apk >/dev/null; then apk update && apk upgrade; else apt-get update -q && "
|
||||
"DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::=--force-confold dist-upgrade; fi")
|
||||
return _im_gast(vmid, befehl, int(p.get("zeitlimit") or 1800))
|
||||
|
||||
|
||||
@@ -1258,9 +1283,14 @@ def a_sicherung_loeschen(p: dict) -> tuple[int, str]:
|
||||
|
||||
|
||||
def a_host_update(p: dict) -> tuple[int, str]:
|
||||
# Kein Neustart: der ist ein eigener Knopf (User-Entscheid 24.09.2026).
|
||||
befehl = ("apt-get update -q && DEBIAN_FRONTEND=noninteractive apt-get -y "
|
||||
"-o Dpkg::Options::=--force-confold dist-upgrade")
|
||||
# Kein Neustart: der ist ein eigener Knopf (User-Entscheid 24.09.2026). Mit „pakete“ (seit 25.09.2026) nur diese.
|
||||
pakete = _paketliste(p)
|
||||
if pakete:
|
||||
befehl = ("apt-get update -q && DEBIAN_FRONTEND=noninteractive apt-get -y "
|
||||
f"-o Dpkg::Options::=--force-confold install --only-upgrade {' '.join(pakete)}")
|
||||
else:
|
||||
befehl = ("apt-get update -q && DEBIAN_FRONTEND=noninteractive apt-get -y "
|
||||
"-o Dpkg::Options::=--force-confold dist-upgrade")
|
||||
return _laufen(["bash", "-c", befehl], 3600)
|
||||
|
||||
|
||||
@@ -1268,7 +1298,6 @@ def a_host_neustart(p: dict) -> tuple[int, str]:
|
||||
return _laufen(["systemctl", "reboot"], 30)
|
||||
|
||||
|
||||
_PAKETNAME = re.compile(r"[a-z0-9][a-z0-9+.-]{1,80}")
|
||||
_KOPF = re.compile(r"^\S+ \(([^)]+)\) ", re.MULTILINE)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user