proxmox-update.py aktualisiert
This commit is contained in:
+463
-150
@@ -19,19 +19,38 @@ ZFS_DATASETS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
SNAPSHOT_TAG = "pve-update-via-cron"
|
SNAPSHOT_TAG = "pve-update-via-cron"
|
||||||
|
|
||||||
|
# Anzahl der ZFS-Snapshots, die je Dataset behalten werden.
|
||||||
MAX_SNAPSHOTS = 5
|
MAX_SNAPSHOTS = 5
|
||||||
|
|
||||||
|
# Anzahl älterer Kernel, die zusätzlich zum aktuell
|
||||||
|
# laufenden Kernel als Fallback behalten werden.
|
||||||
|
#
|
||||||
|
# 0 = nur aktiven Kernel behalten
|
||||||
|
# 1 = aktiven + 1 Fallback
|
||||||
|
# 2 = aktiven + 2 Fallback-Kernel
|
||||||
|
KEEP_OLD_KERNELS = 1
|
||||||
|
|
||||||
LOGFILE = "/var/log/proxmox_update.log"
|
LOGFILE = "/var/log/proxmox_update.log"
|
||||||
|
|
||||||
REBOOT_FLAG = os.environ.get("REBOOT", "NO").upper()
|
REBOOT_FLAG = os.environ.get("REBOOT", "NO").upper()
|
||||||
|
|
||||||
TIMESTAMP = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
TIMESTAMP = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||||
|
|
||||||
# Cron-Jobs laufen mit einem minimalen PATH (oft nur /usr/bin:/bin).
|
|
||||||
# proxmox-boot-tool & Co. liegen aber in /usr/sbin bzw. /sbin.
|
# Unterstützt sowohl historische pve-kernel-* Pakete
|
||||||
# Deshalb PATH hier explizit erweitern, damit es unter cron genauso
|
# als auch aktuelle proxmox-kernel-* Pakete.
|
||||||
# funktioniert wie in einer interaktiven root-Shell.
|
KERNEL_PKG_PATTERN = re.compile(
|
||||||
os.environ["PATH"] = "/usr/sbin:/usr/bin:/sbin:/bin:" + os.environ.get("PATH", "")
|
r"^(?:pve-kernel|proxmox-kernel)-([0-9][^\s]*?)(?:-signed)?$"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Cron-Jobs laufen mit einem minimalen PATH.
|
||||||
|
# Proxmox-Tools liegen teilweise in /usr/sbin bzw. /sbin.
|
||||||
|
os.environ["PATH"] = (
|
||||||
|
"/usr/sbin:/usr/bin:/sbin:/bin:"
|
||||||
|
+ os.environ.get("PATH", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
@@ -46,7 +65,6 @@ logging.basicConfig(
|
|||||||
|
|
||||||
|
|
||||||
def log(msg, level="info"):
|
def log(msg, level="info"):
|
||||||
|
|
||||||
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
print(f"[{timestamp}] {msg}")
|
print(f"[{timestamp}] {msg}")
|
||||||
@@ -67,13 +85,13 @@ def log(msg, level="info"):
|
|||||||
|
|
||||||
def run(cmd, check=True, quiet=False):
|
def run(cmd, check=True, quiet=False):
|
||||||
"""
|
"""
|
||||||
cmd darf entweder eine Liste von Argumenten sein (kein Shell-Aufruf,
|
Führt einen Befehl ohne Shell aus.
|
||||||
sicherer) oder ein String OHNE Shell-Metazeichen (Pipes, Redirects).
|
|
||||||
Für Pipelines gibt es run_pipeline().
|
|
||||||
|
|
||||||
quiet=True unterdrückt das automatische Loggen der stdout-Ausgabe
|
cmd kann eine Argumentliste oder ein String ohne
|
||||||
(nützlich, wenn der Aufrufer die Ausgabe erst selbst filtert und
|
Shell-Metazeichen sein.
|
||||||
danach gezielt loggt).
|
|
||||||
|
quiet=True unterdrückt das automatische Loggen
|
||||||
|
von stdout.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if isinstance(cmd, str):
|
if isinstance(cmd, str):
|
||||||
@@ -84,13 +102,12 @@ def run(cmd, check=True, quiet=False):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
cmd,
|
cmd,
|
||||||
check=check,
|
check=check,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
text=True
|
text=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.stdout.strip() and not quiet:
|
if result.stdout.strip() and not quiet:
|
||||||
@@ -104,8 +121,9 @@ def run(cmd, check=True, quiet=False):
|
|||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
|
|
||||||
log(
|
log(
|
||||||
f"FEHLER: Befehl nicht gefunden: {' '.join(cmd)} ({e})",
|
f"FEHLER: Befehl nicht gefunden: "
|
||||||
"error"
|
f"{' '.join(cmd)} ({e})",
|
||||||
|
"error",
|
||||||
)
|
)
|
||||||
|
|
||||||
if check:
|
if check:
|
||||||
@@ -117,52 +135,15 @@ def run(cmd, check=True, quiet=False):
|
|||||||
|
|
||||||
log(
|
log(
|
||||||
f"FEHLER: {' '.join(cmd)}",
|
f"FEHLER: {' '.join(cmd)}",
|
||||||
"error"
|
"error",
|
||||||
)
|
)
|
||||||
|
|
||||||
if e.stderr:
|
if e.stderr:
|
||||||
log(e.stderr.strip(), "error")
|
log(
|
||||||
|
e.stderr.strip(),
|
||||||
if check:
|
"error",
|
||||||
raise
|
|
||||||
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def run_pipeline(cmd_str, check=False):
|
|
||||||
"""
|
|
||||||
Führt einen Shell-Befehl MIT Pipes/Redirects tatsächlich über eine Shell
|
|
||||||
aus (shell=True). Nur für fest im Skript definierte, ungefährliche
|
|
||||||
Kommandos verwenden - niemals mit Benutzereingaben füttern.
|
|
||||||
"""
|
|
||||||
|
|
||||||
log(f"Ausführen (shell): {cmd_str}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
cmd_str,
|
|
||||||
shell=True,
|
|
||||||
check=check,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
text=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.stdout.strip():
|
|
||||||
log(result.stdout.strip())
|
|
||||||
|
|
||||||
if result.stderr.strip():
|
|
||||||
log(result.stderr.strip(), "warn")
|
|
||||||
|
|
||||||
return result.stdout.strip()
|
|
||||||
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
|
|
||||||
log(f"FEHLER: {cmd_str}", "error")
|
|
||||||
|
|
||||||
if e.stderr:
|
|
||||||
log(e.stderr.strip(), "error")
|
|
||||||
|
|
||||||
if check:
|
if check:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@@ -174,29 +155,54 @@ def run_pipeline(cmd_str, check=False):
|
|||||||
# =========================
|
# =========================
|
||||||
|
|
||||||
def get_running_kernel():
|
def get_running_kernel():
|
||||||
|
|
||||||
return run([
|
return run([
|
||||||
"uname",
|
"uname",
|
||||||
"-r"
|
"-r",
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
KERNEL_PKG_PATTERN = re.compile(r"^(?:pve-kernel|proxmox-kernel)-([0-9][^\s]*?)(?:-signed)?$")
|
def version_key(version):
|
||||||
|
"""
|
||||||
|
Sortierbarer Schlüssel für Kernel-Versionen.
|
||||||
|
|
||||||
|
Beispiel:
|
||||||
|
6.14.8
|
||||||
|
6.14.10
|
||||||
|
6.14.11
|
||||||
|
"""
|
||||||
|
|
||||||
|
return [
|
||||||
|
int(part) if part.isdigit() else part
|
||||||
|
for part in re.split(r"[.\-]", version)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _query_kernel_packages():
|
def _query_kernel_packages():
|
||||||
"""
|
"""
|
||||||
Fragt dpkg nach allen proxmox-kernel-* Paketen ab und gibt eine Liste
|
Fragt dpkg nach beiden Proxmox-Kernel-Paketgruppen ab.
|
||||||
von (status, paketname) Tupeln zurück. Die rohe dpkg-Ausgabe wird NICHT
|
|
||||||
geloggt (quiet=True), da sie auch nicht-installierte ("un") und nur
|
Zurückgegeben werden Tupel:
|
||||||
noch als Konfigurationsreste vorhandene ("rc") Pakete enthält, die wir
|
|
||||||
im Log nicht als "installiert" anzeigen wollen.
|
(status, paketname)
|
||||||
|
|
||||||
|
Beispiel:
|
||||||
|
|
||||||
|
("ii", "proxmox-kernel-6.14.11-1-pve-signed")
|
||||||
|
("rc", "proxmox-kernel-6.8.12-2-pve-signed")
|
||||||
|
|
||||||
|
Nur "ii" bedeutet tatsächlich installiert.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
output = run(
|
output = run(
|
||||||
["dpkg-query", "-W", "-f=${db:Status-Abbrev} ${Package}\n",
|
[
|
||||||
"proxmox-kernel-*"],
|
"dpkg-query",
|
||||||
|
"-W",
|
||||||
|
"-f=${db:Status-Abbrev} ${Package}\n",
|
||||||
|
"pve-kernel-*",
|
||||||
|
"proxmox-kernel-*",
|
||||||
|
],
|
||||||
check=False,
|
check=False,
|
||||||
quiet=True
|
quiet=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
packages = []
|
packages = []
|
||||||
@@ -210,67 +216,287 @@ def _query_kernel_packages():
|
|||||||
|
|
||||||
status, pkg = parts
|
status, pkg = parts
|
||||||
|
|
||||||
packages.append((status, pkg))
|
packages.append(
|
||||||
|
(status, pkg)
|
||||||
|
)
|
||||||
|
|
||||||
return packages
|
return packages
|
||||||
|
|
||||||
|
|
||||||
def get_installed_kernel():
|
def get_installed_kernel_versions():
|
||||||
"""
|
"""
|
||||||
Ermittelt die höchste installierte Kernel-Version rein in Python
|
Liefert tatsächlich installierte Proxmox-Kernel.
|
||||||
(kein fragiles Shell-Pipeline-Kommando mehr, kein sort -V nötig).
|
|
||||||
Loggt dabei NUR die tatsächlich installierten ("ii") Kernel-Pakete,
|
Nur dpkg-Status "ii" wird berücksichtigt.
|
||||||
keine "un" (nicht installiert) oder "rc" (entfernt, Reste vorhanden).
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
packages = _query_kernel_packages()
|
packages = _query_kernel_packages()
|
||||||
|
|
||||||
installed_pkgs = [pkg for status, pkg in packages if status == "ii"]
|
kernels = []
|
||||||
|
|
||||||
if installed_pkgs:
|
for status, pkg in packages:
|
||||||
log(
|
|
||||||
"Installierte Kernel-Pakete: " + ", ".join(sorted(installed_pkgs))
|
|
||||||
)
|
|
||||||
|
|
||||||
versions = []
|
if status != "ii":
|
||||||
|
|
||||||
for pkg in installed_pkgs:
|
|
||||||
|
|
||||||
m = KERNEL_PKG_PATTERN.match(pkg)
|
|
||||||
|
|
||||||
if not m:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
versions.append(m.group(1))
|
match = KERNEL_PKG_PATTERN.match(pkg)
|
||||||
|
|
||||||
if not versions:
|
if not match:
|
||||||
|
continue
|
||||||
|
|
||||||
|
kernels.append({
|
||||||
|
"version": match.group(1),
|
||||||
|
"package": pkg,
|
||||||
|
})
|
||||||
|
|
||||||
|
return kernels
|
||||||
|
|
||||||
|
|
||||||
|
def get_installed_kernel():
|
||||||
|
"""
|
||||||
|
Ermittelt die höchste installierte Proxmox-Kernel-Version.
|
||||||
|
"""
|
||||||
|
|
||||||
|
kernels = get_installed_kernel_versions()
|
||||||
|
|
||||||
|
if not kernels:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
# Versionssortierung ohne externes sort -V:
|
versions = sorted(
|
||||||
def version_key(v):
|
{
|
||||||
return [int(p) if p.isdigit() else p for p in re.split(r"[.\-]", v)]
|
kernel["version"]
|
||||||
|
for kernel in kernels
|
||||||
|
},
|
||||||
|
key=version_key,
|
||||||
|
)
|
||||||
|
|
||||||
versions.sort(key=version_key)
|
log(
|
||||||
|
"Installierte Proxmox-Kernel: "
|
||||||
|
+ ", ".join(versions)
|
||||||
|
)
|
||||||
|
|
||||||
return versions[-1]
|
return versions[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_old_kernels():
|
||||||
|
"""
|
||||||
|
Entfernt alte installierte Proxmox-Kernel.
|
||||||
|
|
||||||
|
Sicherheitsregeln:
|
||||||
|
|
||||||
|
- Der aktuell laufende Kernel wird niemals gelöscht.
|
||||||
|
- Ist der laufende Kernel nicht eindeutig in der
|
||||||
|
installierten Kernel-Liste vorhanden, wird nichts gelöscht.
|
||||||
|
- KEEP_OLD_KERNELS ältere Kernel bleiben erhalten.
|
||||||
|
- Ein neuerer installierter Kernel als der laufende
|
||||||
|
wird ebenfalls nicht gelöscht, da er nach einem
|
||||||
|
Reboot benötigt werden kann.
|
||||||
|
- Nur Status "ii" wird hier berücksichtigt.
|
||||||
|
- Entfernen erfolgt über apt-get purge.
|
||||||
|
"""
|
||||||
|
|
||||||
|
running = get_running_kernel()
|
||||||
|
|
||||||
|
if not running:
|
||||||
|
log(
|
||||||
|
"⚠️ Aktiver Kernel konnte nicht ermittelt werden - "
|
||||||
|
"keine Kernel werden gelöscht",
|
||||||
|
"warn",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
kernels = get_installed_kernel_versions()
|
||||||
|
|
||||||
|
if not kernels:
|
||||||
|
log(
|
||||||
|
"⚠️ Keine installierten Proxmox-Kernel erkannt - "
|
||||||
|
"keine Kernel werden gelöscht",
|
||||||
|
"warn",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Version -> Pakete
|
||||||
|
version_packages = {}
|
||||||
|
|
||||||
|
for kernel in kernels:
|
||||||
|
|
||||||
|
version = kernel["version"]
|
||||||
|
package = kernel["package"]
|
||||||
|
|
||||||
|
version_packages.setdefault(
|
||||||
|
version,
|
||||||
|
[]
|
||||||
|
).append(package)
|
||||||
|
|
||||||
|
installed_versions = sorted(
|
||||||
|
version_packages.keys(),
|
||||||
|
key=version_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
log(
|
||||||
|
f"🖥️ Aktiver Kernel: {running}"
|
||||||
|
)
|
||||||
|
|
||||||
|
log(
|
||||||
|
"📋 Installierte Proxmox-Kernel:"
|
||||||
|
)
|
||||||
|
|
||||||
|
for version in reversed(installed_versions):
|
||||||
|
|
||||||
|
marker = ""
|
||||||
|
|
||||||
|
if version == running:
|
||||||
|
marker = " ← AKTIV"
|
||||||
|
|
||||||
|
log(
|
||||||
|
f" {version}{marker}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Der laufende Kernel muss exakt als installierte
|
||||||
|
# Kernel-Version vorhanden sein.
|
||||||
|
active_matches = [
|
||||||
|
version
|
||||||
|
for version in installed_versions
|
||||||
|
if version == running
|
||||||
|
]
|
||||||
|
|
||||||
|
if len(active_matches) != 1:
|
||||||
|
|
||||||
|
log(
|
||||||
|
"⚠️ Aktiver Kernel wurde nicht eindeutig in den "
|
||||||
|
"installierten Kernel-Paketen gefunden - "
|
||||||
|
"keine Kernel werden gelöscht",
|
||||||
|
"warn",
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
active_index = installed_versions.index(running)
|
||||||
|
|
||||||
|
# Alles unterhalb des laufenden Kernels ist älter.
|
||||||
|
older_versions = installed_versions[:active_index]
|
||||||
|
|
||||||
|
# Die direkt älteren KEEP_OLD_KERNELS Versionen bleiben.
|
||||||
|
fallback_versions = older_versions[
|
||||||
|
max(0, len(older_versions) - KEEP_OLD_KERNELS):
|
||||||
|
]
|
||||||
|
|
||||||
|
for version in reversed(fallback_versions):
|
||||||
|
|
||||||
|
log(
|
||||||
|
f"🛡️ Fallback-Kernel bleibt erhalten: {version}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sicherheitsregel:
|
||||||
|
#
|
||||||
|
# Versionen oberhalb des laufenden Kernels werden NICHT
|
||||||
|
# gelöscht. Das kann ein neuer Kernel sein, auf den nach
|
||||||
|
# dem Reboot gewechselt werden soll.
|
||||||
|
newer_versions = [
|
||||||
|
version
|
||||||
|
for version in installed_versions
|
||||||
|
if version_key(version) > version_key(running)
|
||||||
|
]
|
||||||
|
|
||||||
|
for version in newer_versions:
|
||||||
|
|
||||||
|
log(
|
||||||
|
f"🛡️ Neuerer installierter Kernel bleibt erhalten: "
|
||||||
|
f"{version} - Reboot kann erforderlich sein",
|
||||||
|
"warn",
|
||||||
|
)
|
||||||
|
|
||||||
|
protected_versions = {
|
||||||
|
running,
|
||||||
|
*fallback_versions,
|
||||||
|
*newer_versions,
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_versions = [
|
||||||
|
version
|
||||||
|
for version in installed_versions
|
||||||
|
if version not in protected_versions
|
||||||
|
]
|
||||||
|
|
||||||
|
if not remove_versions:
|
||||||
|
|
||||||
|
log(
|
||||||
|
"OK: Keine alten Kernel zur Entfernung vorhanden"
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
for version in remove_versions:
|
||||||
|
|
||||||
|
packages_to_remove = version_packages.get(
|
||||||
|
version,
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
for package in packages_to_remove:
|
||||||
|
|
||||||
|
log(
|
||||||
|
f"🗑️ Entferne alten Kernel: {package}"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
"apt-get",
|
||||||
|
"purge",
|
||||||
|
"-y",
|
||||||
|
package,
|
||||||
|
],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
|
||||||
|
log(
|
||||||
|
f"✅ Kernel entfernt: {package}"
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
log(
|
||||||
|
f"❌ Fehler beim Entfernen von {package} "
|
||||||
|
f"(Exit-Code {result.returncode})",
|
||||||
|
"error",
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.stderr.strip():
|
||||||
|
|
||||||
|
log(
|
||||||
|
result.stderr.strip(),
|
||||||
|
"error",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Absichtlich kein raise:
|
||||||
|
# Fehler bei einem einzelnen Kernel sollen
|
||||||
|
# den restlichen Update-Lauf nicht abbrechen.
|
||||||
|
|
||||||
|
|
||||||
def cleanup_old_kernel_configs():
|
def cleanup_old_kernel_configs():
|
||||||
"""
|
"""
|
||||||
Entfernt Konfigurationsreste von bereits deinstallierten alten
|
Entfernt ausschließlich dpkg-Konfigurationsreste ("rc")
|
||||||
Kernel-Paketen (dpkg-Status "rc" = removed, config-files still present).
|
alter Proxmox-Kernel-Pakete.
|
||||||
Diese Reste belegen unnötig Platz und tauchen sonst dauerhaft in
|
|
||||||
'dpkg -l' auf, obwohl das Paket längst deinstalliert ist.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
packages = _query_kernel_packages()
|
packages = _query_kernel_packages()
|
||||||
|
|
||||||
residual_pkgs = [pkg for status, pkg in packages if status == "rc"]
|
residual_pkgs = [
|
||||||
|
pkg
|
||||||
|
for status, pkg in packages
|
||||||
|
if status == "rc"
|
||||||
|
]
|
||||||
|
|
||||||
if not residual_pkgs:
|
if not residual_pkgs:
|
||||||
|
|
||||||
log(
|
log(
|
||||||
"OK: Keine Konfigurationsreste alter Kernel-Pakete gefunden"
|
"OK: Keine Konfigurationsreste alter Kernel-Pakete gefunden"
|
||||||
)
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
residual_pkgs = sorted(residual_pkgs)
|
residual_pkgs = sorted(residual_pkgs)
|
||||||
@@ -278,30 +504,64 @@ def cleanup_old_kernel_configs():
|
|||||||
total = len(residual_pkgs)
|
total = len(residual_pkgs)
|
||||||
|
|
||||||
log(
|
log(
|
||||||
f"Entferne Konfigurationsreste von {total} alten Kernel-Paketen"
|
f"🧼 Entferne Konfigurationsreste von "
|
||||||
|
f"{total} alten Kernel-Paketen"
|
||||||
)
|
)
|
||||||
|
|
||||||
for index, pkg in enumerate(residual_pkgs, start=1):
|
for index, pkg in enumerate(
|
||||||
|
residual_pkgs,
|
||||||
|
start=1,
|
||||||
|
):
|
||||||
|
|
||||||
log(
|
log(
|
||||||
f"({index}/{total}) Lösche Reste von: {pkg}"
|
f"({index}/{total}) Lösche Reste von: {pkg}"
|
||||||
)
|
)
|
||||||
|
|
||||||
run(
|
result = subprocess.run(
|
||||||
["apt-get", "purge", "-y", "-qq", pkg],
|
[
|
||||||
check=False,
|
"apt-get",
|
||||||
quiet=True
|
"purge",
|
||||||
|
"-y",
|
||||||
|
"-qq",
|
||||||
|
pkg,
|
||||||
|
],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
|
||||||
log(
|
log(
|
||||||
f"OK: ({index}/{total}) Erledigt: {pkg}"
|
f"✅ ({index}/{total}) Erledigt: {pkg}"
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
log(
|
||||||
|
f"❌ ({index}/{total}) Fehler beim Entfernen "
|
||||||
|
f"der Reste: {pkg}",
|
||||||
|
"error",
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.stderr.strip():
|
||||||
|
|
||||||
|
log(
|
||||||
|
result.stderr.strip(),
|
||||||
|
"error",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def reboot_required():
|
def reboot_required():
|
||||||
|
"""
|
||||||
|
Prüft getrennt von der Kernelbereinigung, ob ein Reboot
|
||||||
|
erforderlich ist.
|
||||||
|
|
||||||
|
Reboot erforderlich, wenn der laufende Kernel nicht
|
||||||
|
der höchsten installierten Kernel-Version entspricht.
|
||||||
|
"""
|
||||||
|
|
||||||
running = get_running_kernel()
|
running = get_running_kernel()
|
||||||
|
|
||||||
installed = get_installed_kernel()
|
installed = get_installed_kernel()
|
||||||
|
|
||||||
log(
|
log(
|
||||||
@@ -313,18 +573,17 @@ def reboot_required():
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not running or not installed:
|
if not running or not installed:
|
||||||
|
|
||||||
log(
|
log(
|
||||||
"WARNUNG: Kernel-Vergleich nicht möglich (leerer Wert) - "
|
"⚠️ Kernel-Vergleich nicht möglich - "
|
||||||
"gehe sicherheitshalber von 'kein Reboot nötig' aus",
|
"gehe sicherheitshalber von 'kein Reboot nötig' aus",
|
||||||
"warn"
|
"warn",
|
||||||
)
|
)
|
||||||
return False
|
|
||||||
|
|
||||||
if running != installed:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
return running != installed
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# Boot Prüfung
|
# Boot Prüfung
|
||||||
@@ -333,26 +592,27 @@ def reboot_required():
|
|||||||
def proxmox_boot_ok():
|
def proxmox_boot_ok():
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[
|
[
|
||||||
"proxmox-boot-tool",
|
"proxmox-boot-tool",
|
||||||
"status"
|
"status",
|
||||||
],
|
],
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
text=True
|
text=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
|
|
||||||
log(
|
log(
|
||||||
f"FEHLER: proxmox-boot-tool nicht gefunden (PATH-Problem?): {e}",
|
f"FEHLER: proxmox-boot-tool nicht gefunden: {e}",
|
||||||
"error"
|
"error",
|
||||||
)
|
)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if result.stdout:
|
if result.stdout.strip():
|
||||||
|
|
||||||
log(
|
log(
|
||||||
result.stdout.strip()
|
result.stdout.strip()
|
||||||
@@ -362,16 +622,19 @@ def proxmox_boot_ok():
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if result.stderr:
|
if result.stderr.strip():
|
||||||
|
|
||||||
log(
|
log(
|
||||||
result.stderr.strip(),
|
result.stderr.strip(),
|
||||||
"error"
|
"error",
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
log(
|
log(
|
||||||
f"FEHLER: proxmox-boot-tool status lieferte Exit-Code {result.returncode} ohne Fehlermeldung",
|
"FEHLER: proxmox-boot-tool status "
|
||||||
"error"
|
f"lieferte Exit-Code {result.returncode}",
|
||||||
|
"error",
|
||||||
)
|
)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
@@ -385,7 +648,7 @@ def refresh_boot():
|
|||||||
|
|
||||||
run([
|
run([
|
||||||
"proxmox-boot-tool",
|
"proxmox-boot-tool",
|
||||||
"refresh"
|
"refresh",
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
@@ -397,16 +660,16 @@ def has_updates():
|
|||||||
|
|
||||||
run([
|
run([
|
||||||
"apt-get",
|
"apt-get",
|
||||||
"update"
|
"update",
|
||||||
])
|
])
|
||||||
|
|
||||||
output = run(
|
output = run(
|
||||||
[
|
[
|
||||||
"apt-get",
|
"apt-get",
|
||||||
"-s",
|
"-s",
|
||||||
"dist-upgrade"
|
"dist-upgrade",
|
||||||
],
|
],
|
||||||
check=False
|
check=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
updates = [
|
updates = [
|
||||||
@@ -431,9 +694,28 @@ def has_updates():
|
|||||||
|
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# Snapshot Funktionen
|
# ZFS Snapshot Funktionen
|
||||||
# =========================
|
# =========================
|
||||||
|
|
||||||
|
def zfs_dataset_exists(dataset):
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
"zfs",
|
||||||
|
"list",
|
||||||
|
"-H",
|
||||||
|
"-o",
|
||||||
|
"name",
|
||||||
|
dataset,
|
||||||
|
],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
def create_snapshot(dataset):
|
def create_snapshot(dataset):
|
||||||
|
|
||||||
snapshot = (
|
snapshot = (
|
||||||
@@ -447,7 +729,7 @@ def create_snapshot(dataset):
|
|||||||
run([
|
run([
|
||||||
"zfs",
|
"zfs",
|
||||||
"snapshot",
|
"snapshot",
|
||||||
snapshot
|
snapshot,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
@@ -463,9 +745,10 @@ def cleanup_snapshots(dataset):
|
|||||||
"-o",
|
"-o",
|
||||||
"name",
|
"name",
|
||||||
"-s",
|
"-s",
|
||||||
"creation"
|
"creation",
|
||||||
],
|
],
|
||||||
check=False
|
check=False,
|
||||||
|
quiet=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
snapshots = []
|
snapshots = []
|
||||||
@@ -494,10 +777,29 @@ def cleanup_snapshots(dataset):
|
|||||||
"zfs",
|
"zfs",
|
||||||
"destroy",
|
"destroy",
|
||||||
"-r",
|
"-r",
|
||||||
snap
|
snap,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def create_update_snapshots():
|
||||||
|
|
||||||
|
for dataset in ZFS_DATASETS:
|
||||||
|
|
||||||
|
if not zfs_dataset_exists(dataset):
|
||||||
|
|
||||||
|
log(
|
||||||
|
f"⚠️ ZFS-Dataset nicht vorhanden: {dataset} - "
|
||||||
|
"Snapshot wird übersprungen, Update läuft weiter",
|
||||||
|
"warn",
|
||||||
|
)
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
|
cleanup_snapshots(dataset)
|
||||||
|
|
||||||
|
create_snapshot(dataset)
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# Upgrade
|
# Upgrade
|
||||||
# =========================
|
# =========================
|
||||||
@@ -505,26 +807,31 @@ def cleanup_snapshots(dataset):
|
|||||||
def upgrade_system():
|
def upgrade_system():
|
||||||
|
|
||||||
log(
|
log(
|
||||||
"Starte dist-upgrade"
|
"⚙️ Starte dist-upgrade"
|
||||||
)
|
)
|
||||||
|
|
||||||
run([
|
run([
|
||||||
"apt-get",
|
"apt-get",
|
||||||
"dist-upgrade",
|
"dist-upgrade",
|
||||||
"-y"
|
"-y",
|
||||||
])
|
])
|
||||||
|
|
||||||
log(
|
log(
|
||||||
"Starte autoremove"
|
"🧼 Starte autoremove"
|
||||||
)
|
)
|
||||||
|
|
||||||
run([
|
run([
|
||||||
"apt-get",
|
"apt-get",
|
||||||
"autoremove",
|
"autoremove",
|
||||||
"--purge",
|
"--purge",
|
||||||
"-y"
|
"-y",
|
||||||
])
|
])
|
||||||
|
|
||||||
|
# Eigene Kernelbereinigung nach dem normalen
|
||||||
|
# Debian/Proxmox-Upgrade.
|
||||||
|
cleanup_old_kernels()
|
||||||
|
|
||||||
|
# Danach eventuell verbliebene rc-Reste bereinigen.
|
||||||
cleanup_old_kernel_configs()
|
cleanup_old_kernel_configs()
|
||||||
|
|
||||||
|
|
||||||
@@ -542,11 +849,12 @@ def main():
|
|||||||
f"REBOOT-Flag: {REBOOT_FLAG}"
|
f"REBOOT-Flag: {REBOOT_FLAG}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Exit-Code 2 für nicht-root beibehalten.
|
||||||
if os.geteuid() != 0:
|
if os.geteuid() != 0:
|
||||||
|
|
||||||
log(
|
log(
|
||||||
"FEHLER: Muss als root laufen",
|
"FEHLER: Muss als root laufen",
|
||||||
"error"
|
"error",
|
||||||
)
|
)
|
||||||
|
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
@@ -559,31 +867,33 @@ def main():
|
|||||||
|
|
||||||
if has_updates():
|
if has_updates():
|
||||||
|
|
||||||
for dataset in ZFS_DATASETS:
|
# Snapshots VOR dem eigentlichen dist-upgrade.
|
||||||
|
create_update_snapshots()
|
||||||
cleanup_snapshots(dataset)
|
|
||||||
|
|
||||||
create_snapshot(dataset)
|
|
||||||
|
|
||||||
upgrade_system()
|
upgrade_system()
|
||||||
|
|
||||||
|
# Nach Kernel-/Paket-Updates Bootloader aktualisieren.
|
||||||
refresh_boot()
|
refresh_boot()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
log(
|
log(
|
||||||
"Keine Updates installiert"
|
"ℹ️ Keine Updates installiert"
|
||||||
)
|
)
|
||||||
|
|
||||||
# auch ohne Update-Lauf vorhandene Reste alter Kernel aufräumen
|
# Auch ohne verfügbare Updates regelmäßig alte
|
||||||
|
# installierte Kernel bereinigen.
|
||||||
|
cleanup_old_kernels()
|
||||||
|
|
||||||
cleanup_old_kernel_configs()
|
cleanup_old_kernel_configs()
|
||||||
|
|
||||||
# immer prüfen
|
# Reboot-Erkennung bleibt vollständig getrennt
|
||||||
|
# von der Kernelbereinigung.
|
||||||
if reboot_required():
|
if reboot_required():
|
||||||
|
|
||||||
log(
|
log(
|
||||||
"WARNUNG: Neuer Kernel aktivieren erforderlich",
|
"⚠️ Neuer Kernel vorhanden - Reboot erforderlich",
|
||||||
"warn"
|
"warn",
|
||||||
)
|
)
|
||||||
|
|
||||||
if REBOOT_FLAG == "YES":
|
if REBOOT_FLAG == "YES":
|
||||||
@@ -599,29 +909,33 @@ def main():
|
|||||||
)
|
)
|
||||||
|
|
||||||
log(
|
log(
|
||||||
"REBOOT=YES erkannt"
|
"REBOOT=YES erkannt - Neustart in 1 Minute"
|
||||||
)
|
)
|
||||||
|
|
||||||
run([
|
run([
|
||||||
"shutdown",
|
"shutdown",
|
||||||
"-r",
|
"-r",
|
||||||
"+1",
|
"+1",
|
||||||
"Proxmox-Update abgeschlossen"
|
"Proxmox-Update abgeschlossen",
|
||||||
])
|
])
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
log(
|
log(
|
||||||
"FEHLER: Bootloader Prüfung fehlgeschlagen - kein Neustart",
|
"❌ Bootloader Prüfung fehlgeschlagen - "
|
||||||
"error"
|
"kein Neustart",
|
||||||
|
"error",
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
log(
|
log(
|
||||||
"REBOOT-Flag ist nicht 'YES' - kein automatischer Neustart"
|
"REBOOT-Flag ist nicht 'YES' - "
|
||||||
|
"kein automatischer Neustart"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Monitoring muss weiterhin erkennen,
|
||||||
|
# dass ein Reboot erforderlich ist.
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
log(
|
log(
|
||||||
@@ -632,5 +946,4 @@ def main():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user