Sandbox-Optionen aus der systemd-Unit entfernen

Als Dienst schlug jeder Snapshot mit "cfs-lock 'storage-data' error: got
lock request timeout" fehl, waehrend derselbe Aufruf in der Shell
funktionierte.

Ursache war ProtectSystem=full in unserer eigenen Unit: das haengt /etc im
Namespace des Dienstes schreibgeschuetzt ein. pvesh fuehrt die Proxmox-API
im eigenen Prozess aus, der Snapshot-Task ist also ein Kindprozess von
pvesnap und erbt diese Einschraenkung. pmxcfs legt seine Sperren aber 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.

ProtectHome=yes war aus demselben Grund schaedlich: es blendet /root aus,
womit die SSH-Schluessel fuer die uebrigen Cluster-Nodes fehlen.

* Unit enthaelt keine Sandbox-Optionen mehr, mit Kommentar, warum nicht.
* Neu: pvesnap/preflight.py prueft root-Rechte, Schreibzugriff auf /etc/pve
  und Sichtbarkeit von /root. Der Dienst schreibt das beim Start ins
  Journal, "pvesnap check" zeigt es ebenfalls an - damit faellt so etwas
  sofort auf, statt sich als Lock-Timeout zu tarnen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
duffyduck
2026-07-31 02:16:12 +02:00
co-authored by Claude Opus 5
parent e26d966a8b
commit 1be13ad294
5 changed files with 126 additions and 12 deletions
+24 -2
View File
@@ -281,8 +281,30 @@ pvesnap geht damit so um:
* Mit `pause_between = 10s` lässt sich zusätzlich Druck vom Storage nehmen, wenn * Mit `pause_between = 10s` lässt sich zusätzlich Druck vom Storage nehmen, wenn
viele VMs auf demselben Storage liegen. viele VMs auf demselben Storage liegen.
Hält die Sperre dauerhaft, liegt die Ursache außerhalb von pvesnap. Diese ### Der Dienst scheitert, von Hand geht es
Kommandos helfen beim Eingrenzen:
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 ```bash
pvesm status # ist das Storage online und erreichbar? pvesm status # ist das Storage online und erreichbar?
+8
View File
@@ -13,6 +13,7 @@ from .config import DEFAULT_CONFIG_PATH, ConfigError, load_config
from .daemon import Daemon, SingleInstanceLock from .daemon import Daemon, SingleInstanceLock
from .engine import run_group, select_guests from .engine import run_group, select_guests
from .naming import parse_name from .naming import parse_name
from .preflight import check_environment
from .proxmox import Proxmox, ProxmoxError, pvesh_available from .proxmox import Proxmox, ProxmoxError, pvesh_available
from .schedule import describe, next_due from .schedule import describe, next_due
from .state import State from .state import State
@@ -331,6 +332,13 @@ def cmd_check(args):
for problem in problems: for problem in problems:
print(" - %s" % problem, file=sys.stderr) print(" - %s" % problem, file=sys.stderr)
return 1 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)) print("Konfiguration in Ordnung: %d Gruppe(n)." % len(config.groups))
for group in config.groups: for group in config.groups:
print(" - %s: %s, behalte %s / %s%s" print(" - %s: %s, behalte %s / %s%s"
+5
View File
@@ -13,6 +13,7 @@ from datetime import datetime
from .config import ConfigError, load_config from .config import ConfigError, load_config
from .engine import run_group from .engine import run_group
from .preflight import warn_about_environment
from .proxmox import Proxmox, ProxmoxError from .proxmox import Proxmox, ProxmoxError
from .schedule import next_due from .schedule import next_due
from .state import State from .state import State
@@ -137,6 +138,10 @@ class Daemon:
len(self.config.groups), globals_.prefix, len(self.config.groups), globals_.prefix,
", TESTLAUF" if globals_.dry_run else "") ", 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( daemon_lock = SingleInstanceLock(
globals_.lock_file + ".daemon", globals_.lock_file + ".daemon",
busy_message="Der pvesnap-Dienst laeuft bereits (%s.daemon)" % globals_.lock_file) busy_message="Der pvesnap-Dienst laeuft bereits (%s.daemon)" % globals_.lock_file)
+73
View File
@@ -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
+16 -10
View File
@@ -14,19 +14,25 @@ Restart=on-failure
RestartSec=30 RestartSec=30
TimeoutStopSec=300 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 User=root
StateDirectory=pvesnap StateDirectory=pvesnap
RuntimeDirectory=pvesnap
# Moderate Absicherung - der Dienst muss auf pvesh und /etc/pve zugreifen. # ACHTUNG: Hier duerfen KEINE Sandbox-Optionen stehen.
NoNewPrivileges=yes #
PrivateTmp=yes # ProtectSystem= haengt /etc schreibgeschuetzt ein. pmxcfs legt seine
ProtectHome=yes # Sperren aber als Verzeichnisse unter /etc/pve/priv/lock/
ProtectSystem=full # an - das mkdir scheitert dann, Proxmox wiederholt es
ProtectKernelTunables=yes # erfolglos und der Task endet in
ProtectControlGroups=yes # "cfs-lock 'storage-XXX' error: got lock request timeout".
RestrictRealtime=yes # 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] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target