homelab: alte Kernel per Knopf entfernen; Systemplatte des Proxmox-Hosts im Blick

User-Wunsch 25.09.: Auf dem Host lagen 25 Kernel (~23 GB), 20 davon hält apt für
entbehrlich (19,7 GB).
- Ausführer: host.kernel_alt/kernel_bleiben aus apt-get -s autoremove (nur
  Kernel, nie laufender, nächster oder festgepinnter), host.systemplatte;
  neue Aktion kernel_aufraeumen bestimmt die Liste selbst und purgt.
- Homelab-Teil: Lauf „kernel“ (Ergebnis aufgeraeumt), POST
  /api/homelab/ziele/pve/kernel-aufraeumen, Vorschlag mit Knopf, Wächter für die
  Systemplatte (80/90 %).
- Oberfläche: Kachel Systemplatte, Knopf „Alte Kernel entfernen“ mit Rückfrage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-25 12:16:14 +02:00
co-authored by Claude Opus 5.5
parent 28888890ec
commit 7a456c8efa
36 changed files with 377 additions and 60 deletions
+83 -3
View File
@@ -637,12 +637,70 @@ 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)
pin = _lesen("/etc/kernel/proxmox-boot-pin").strip() or None
host["kernel_neu"] = neuer_kernel(host["kernel"], _kernel_vorhanden(), pin)
host["neustart_noetig"] = host["neustart_noetig"] or bool(host["kernel_neu"])
host["systemplatte"] = _platte("/")
try:
host["kernel_alt"], host["kernel_bleiben"] = _kernel_lage(host["kernel"], host["kernel_neu"] or host["kernel"], pin)
except Exception:
host["kernel_alt"], host["kernel_bleiben"] = [], []
return host
def _platte(pfad: str) -> dict | None:
"""Belegung wie df sie zeigt: belegt + was Nutzer noch schreiben dürfen (ohne die Reserve für root)."""
try:
s = os.statvfs(pfad)
except (OSError, AttributeError): # AttributeError: Windows kennt statvfs nicht (Tests am PC)
return None
belegt = (s.f_blocks - s.f_bfree) * s.f_frsize
return {"belegt": belegt, "gesamt": belegt + s.f_bavail * s.f_frsize}
def _kernel_vorhanden() -> list[str]:
return [os.path.basename(p)[len("vmlinuz-"):] for p in glob.glob("/boot/vmlinuz-*")]
KERNEL_PAKET = re.compile(r"^(?:proxmox|pve)-kernel-(\d+\.\d+\.\d+-\d+-pve)(?:-signed)?$")
def alte_kernel(simulation: str, behalten: set[str]) -> list[str]:
"""Kernel-Pakete, die `apt-get -s autoremove` entfernen würde — nur Kernel und nie einer aus `behalten` (der
laufende, der beim nächsten Start kommt, ein festgepinnter). Was Proxmox selbst hält (je Serie den jüngsten, den
bei der Installation), steht gar nicht erst in der Simulation."""
pakete = []
for zeile in simulation.splitlines():
m = re.match(r"^Remv (\S+)", zeile)
k = KERNEL_PAKET.match(m.group(1)) if m else None
if k and k.group(1) not in behalten:
pakete.append(m.group(1))
return pakete
def _kernel_lage(laufend: str | None, naechster: str | None, pin: str | None) -> tuple[list[dict], list[str]]:
"""(entbehrliche Kernel mit Paket, Version und belegtem Platz samt initrd; die bleibenden Versionen)."""
code, simulation = _laufen(["apt-get", "-s", "autoremove"], 120)
if code != 0 or not laufend:
return [], []
pakete = alte_kernel(simulation, {k for k in (laufend, naechster, pin) if k})
groessen: dict[str, int] = {}
if pakete:
code, aus = _laufen(["dpkg-query", "-W", "-f=${Package}\t${Installed-Size}\n", *pakete], 30)
for zeile in aus.splitlines() if code == 0 else []:
name, _, kib = zeile.partition("\t")
if kib.strip().isdigit():
groessen[name] = int(kib) * 1024
weg = []
for paket in pakete:
version = KERNEL_PAKET.match(paket).group(1)
initrd = f"/boot/initrd.img-{version}"
weg.append({"paket": paket, "version": version,
"groesse": groessen.get(paket, 0) + (os.path.getsize(initrd) if os.path.exists(initrd) else 0)})
entbehrlich = {w["version"] for w in weg}
return weg, sorted((v for v in _kernel_vorhanden() if v not in entbehrlich), key=_versionsschluessel)
def _versionsschluessel(version: str) -> tuple[int, ...]:
return tuple(int(x) for x in re.findall(r"\d+", version))
@@ -843,11 +901,33 @@ def a_bericht(p: dict) -> tuple[int, str]:
return 0, json.dumps(bericht(), ensure_ascii=False)
def a_kernel_aufraeumen(p: dict) -> tuple[int, str]:
"""Alte Kernel entfernen (seit 25.09.2026, nur auf Knopfdruck). Welche, bestimmt dieser Host selbst, jetzt und wie
im Bericht — der Server kann nur den Anstoß geben. Am Ende: entfernt=<Anzahl>, frei=<Bytes>."""
code, laufend = _laufen(["uname", "-r"], 5)
if code != 0 or not laufend.strip():
raise ValueError("der laufende Kernel ließ sich nicht lesen")
laufend = laufend.strip()
pin = _lesen("/etc/kernel/proxmox-boot-pin").strip() or None
naechster = neuer_kernel(laufend, _kernel_vorhanden(), pin) or laufend
weg, _ = _kernel_lage(laufend, naechster, pin)
pakete = [w["paket"] for w in weg if w["version"] not in {laufend, naechster, pin}]
if not pakete:
return 0, "entfernt=0\nfrei=0\nKein alter Kernel zu entfernen."
vorher = _platte("/") or {}
code, text = _laufen(["env", "DEBIAN_FRONTEND=noninteractive", "apt-get", "-y", "purge", *pakete], 30 * 60,
sanft=True)
nachher = _platte("/") or {}
frei = max(0, int(vorher.get("belegt") or 0) - int(nachher.get("belegt") or 0))
return code, f"entfernt={len(pakete) if code == 0 else 0}\nfrei={frei}\n{text[-1500:]}"
AKTIONEN = {"snapshot": a_snapshot, "update": a_update, "os_update": a_os_update, "suchen": a_suchen,
"zurueck": a_zurueck,
"snapshot_loeschen": a_snapshot_loeschen, "host_update": a_host_update,
"host_neustart": a_host_neustart, "bericht": a_bericht,
"sichern": a_sichern, "sicherung_zurueck": a_sicherung_zurueck, "sicherung_loeschen": a_sicherung_loeschen}
"sichern": a_sichern, "sicherung_zurueck": a_sicherung_zurueck, "sicherung_loeschen": a_sicherung_loeschen,
"kernel_aufraeumen": a_kernel_aufraeumen}
def ausfuehren(auftrag: dict) -> dict: