diff --git a/README.md b/README.md index 8ac5b87..ca9a192 100644 --- a/README.md +++ b/README.md @@ -281,8 +281,30 @@ pvesnap geht damit so um: * Mit `pause_between = 10s` lässt sich zusätzlich Druck vom Storage nehmen, wenn viele VMs auf demselben Storage liegen. -Hält die Sperre dauerhaft, liegt die Ursache außerhalb von pvesnap. Diese -Kommandos helfen beim Eingrenzen: +### Der Dienst scheitert, von Hand geht es + +Scheitert **jeder** Lauf des Dienstes am Storage-Lock, während derselbe Befehl +in der Shell funktioniert, liegt es an der Umgebung des Dienstes — nicht am +Storage. `pvesh` führt die Proxmox-API im eigenen Prozess aus, der Snapshot-Task +ist also ein *Kindprozess von pvesnap* und erbt alles, was in der systemd-Unit +eingeschränkt wurde. + +Der Klassiker ist `ProtectSystem=` in der Unit: das hängt `/etc` schreibgeschützt +ein, und pmxcfs legt seine Sperren als Verzeichnisse unter `/etc/pve/priv/lock/` +an. Das `mkdir` scheitert, Proxmox wiederholt es erfolglos — und meldet am Ende +einen Lock-Timeout statt eines Rechtefehlers. Prüfen: + +```bash +systemctl show pvesnap -p ProtectSystem -p ProtectHome -p PrivateTmp -p ReadOnlyPaths +pvesnap check # meldet so etwas von sich aus +``` + +Alle drei müssen leer bzw. `no` sein. Die mitgelieferte Unit enthält deshalb +bewusst **keine** Sandbox-Optionen. + +### Wenn die Sperre wirklich belegt ist + +Diese Kommandos helfen beim Eingrenzen: ```bash pvesm status # ist das Storage online und erreichbar? diff --git a/pvesnap/cli.py b/pvesnap/cli.py index e9ca155..8dfead7 100644 --- a/pvesnap/cli.py +++ b/pvesnap/cli.py @@ -13,6 +13,7 @@ from .config import DEFAULT_CONFIG_PATH, ConfigError, load_config from .daemon import Daemon, SingleInstanceLock from .engine import run_group, select_guests from .naming import parse_name +from .preflight import check_environment from .proxmox import Proxmox, ProxmoxError, pvesh_available from .schedule import describe, next_due from .state import State @@ -331,6 +332,13 @@ def cmd_check(args): for problem in problems: print(" - %s" % problem, file=sys.stderr) return 1 + environment = check_environment(require_pve=pvesh_available()) + if environment: + print("Hinweise zur Umgebung:", file=sys.stderr) + for problem in environment: + print(" - %s" % problem, file=sys.stderr) + print("", file=sys.stderr) + print("Konfiguration in Ordnung: %d Gruppe(n)." % len(config.groups)) for group in config.groups: print(" - %s: %s, behalte %s / %s%s" diff --git a/pvesnap/daemon.py b/pvesnap/daemon.py index 96d3f72..3299426 100644 --- a/pvesnap/daemon.py +++ b/pvesnap/daemon.py @@ -13,6 +13,7 @@ from datetime import datetime from .config import ConfigError, load_config from .engine import run_group +from .preflight import warn_about_environment from .proxmox import Proxmox, ProxmoxError from .schedule import next_due from .state import State @@ -137,6 +138,10 @@ class Daemon: len(self.config.groups), globals_.prefix, ", TESTLAUF" if globals_.dry_run else "") + # Der Dienst laeuft trotzdem weiter - aber es soll gleich beim Start im + # Journal stehen, warum spaeter jeder Snapshot scheitert. + warn_about_environment(log) + daemon_lock = SingleInstanceLock( globals_.lock_file + ".daemon", busy_message="Der pvesnap-Dienst laeuft bereits (%s.daemon)" % globals_.lock_file) diff --git a/pvesnap/preflight.py b/pvesnap/preflight.py new file mode 100644 index 0000000..9efa9ef --- /dev/null +++ b/pvesnap/preflight.py @@ -0,0 +1,73 @@ +"""Prueft, ob pvesnap ueberhaupt arbeiten kann. + +Hintergrund: pvesh fuehrt die Proxmox-API im eigenen Prozess aus. Der +Snapshot-Task ist damit ein Kindprozess von pvesnap und erbt dessen Umgebung - +also auch Einschraenkungen aus der systemd-Unit. Eine schreibgeschuetzte /etc +faellt dabei nicht als Rechtefehler auf, sondern als +"cfs-lock 'storage-XXX' error: got lock request timeout", weil pmxcfs seine +Sperren als Verzeichnisse unter /etc/pve/priv/lock/ anlegt. +""" + +from __future__ import annotations + +import os + +from .proxmox import pvesh_available + +PMXCFS = "/etc/pve" +PMXCFS_LOCKDIR = "/etc/pve/priv/lock" + + +def _read_only(path): + """True, wenn `path` auf einem schreibgeschuetzt eingehaengten Dateisystem liegt.""" + try: + stats = os.statvfs(path) + except OSError: + return False + return bool(stats.f_flag & getattr(os, "ST_RDONLY", 1)) + + +def check_environment(require_pve=True): + """Liste von Problemen; leer heisst: alles in Ordnung.""" + problems = [] + + if os.geteuid() != 0: + problems.append("pvesnap muss als root laufen (pvesh benoetigt root-Rechte).") + + if require_pve and not pvesh_available(): + problems.append("'pvesh' nicht gefunden - laeuft pvesnap auf einem " + "Proxmox-VE-Host?") + + if not os.path.isdir(PMXCFS): + if require_pve: + problems.append("%s nicht vorhanden - laeuft pve-cluster?" % PMXCFS) + return problems + + if _read_only(PMXCFS) or not os.access(PMXCFS, os.W_OK): + problems.append( + "%s ist nicht beschreibbar. Snapshots scheitern dann mit " + "\"cfs-lock 'storage-...' error: got lock request timeout\", weil " + "Proxmox seine Sperren unter %s anlegt. Ursache ist meist eine " + "Sandbox-Option in der systemd-Unit (ProtectSystem=, ReadOnlyPaths=) " + "- pruefen mit: systemctl show pvesnap -p ProtectSystem -p ProtectHome" + % (PMXCFS, PMXCFS_LOCKDIR)) + + # ProtectHome=yes ersetzt /root durch ein leeres, unlesbares Verzeichnis. + try: + empty_root = os.path.isdir("/root") and not os.listdir("/root") + except OSError: + empty_root = True + if empty_root: + problems.append("/root ist leer oder nicht lesbar (ProtectHome= in der " + "systemd-Unit?). Proxmox erreicht andere Nodes im Cluster " + "ueber die SSH-Schluessel in /root/.ssh.") + + return problems + + +def warn_about_environment(log, require_pve=True): + """Probleme protokollieren; gibt True zurueck, wenn alles passt.""" + problems = check_environment(require_pve=require_pve) + for problem in problems: + log.error("Umgebung: %s", problem) + return not problems diff --git a/systemd/pvesnap.service b/systemd/pvesnap.service index 5c01efc..a3c077b 100644 --- a/systemd/pvesnap.service +++ b/systemd/pvesnap.service @@ -14,19 +14,25 @@ Restart=on-failure RestartSec=30 TimeoutStopSec=300 -# pvesh benoetigt root-Rechte. +# pvesh fuehrt die API im eigenen Prozess aus - der Snapshot-Task ist also ein +# Kindprozess von pvesnap und erbt dessen Umgebung. User=root StateDirectory=pvesnap -RuntimeDirectory=pvesnap -# Moderate Absicherung - der Dienst muss auf pvesh und /etc/pve zugreifen. -NoNewPrivileges=yes -PrivateTmp=yes -ProtectHome=yes -ProtectSystem=full -ProtectKernelTunables=yes -ProtectControlGroups=yes -RestrictRealtime=yes +# ACHTUNG: Hier duerfen KEINE Sandbox-Optionen stehen. +# +# ProtectSystem= haengt /etc schreibgeschuetzt ein. pmxcfs legt seine +# Sperren aber als Verzeichnisse unter /etc/pve/priv/lock/ +# an - das mkdir scheitert dann, Proxmox wiederholt es +# erfolglos und der Task endet in +# "cfs-lock 'storage-XXX' error: got lock request timeout". +# ProtectHome= blendet /root aus - damit fehlen die SSH-Schluessel, mit +# denen Proxmox andere Nodes im Cluster erreicht. +# PrivateTmp= gibt dem Dienst ein eigenes /tmp, das Proxmox-Werkzeuge +# nicht mit den uebrigen pve-Diensten teilen. +# +# Die Proxmox-eigenen Units (pvedaemon, pvescheduler) verzichten aus denselben +# Gruenden auf diese Optionen. [Install] WantedBy=multi-user.target