Snapshot-Laeufe gegen cfs-Sperren des Storages absichern
Auf einem echten Host schlugen Snapshots reihenweise mit
"cfs-lock 'storage-NAME' error: got lock request timeout" fehl.
Ursache: 'pvesh create .../snapshot' lief mit dem allgemeinen
Kommando-Zeitlimit von 60s. Genau so lange wartet Proxmox aber auf den
Storage-Lock. Lief der Aufruf in unser Zeitlimit, ging es mit der naechsten
VM weiter, waehrend der Task noch lief - und die naechste VM scheiterte
dann an derselben Sperre. Eine VM konnte so einen ganzen Lauf umwerfen.
* Snapshot-Aktionen laufen jetzt mit dem langen task_timeout statt mit dem
kurzen Zeitlimit fuer Lesezugriffe.
* Ohne UPID in der Antwort wird ersatzweise gewartet, bis der Gast nicht
mehr gesperrt ist, statt sofort weiterzumachen.
* Sperr-Fehler gelten als voruebergehend und werden 'retries'-mal mit
'retry_delay' Abstand wiederholt; echte Fehler wie "storage does not
support snapshots" nicht.
* Neu: 'pause_between' fuer eine Pause zwischen zwei Gaesten.
Ausserdem: Kommentare hinter einem Wert ("retries = 2 # ...") wurden nicht
abgeschnitten und machten die Konfiguration ungueltig - das eigene
Beispiel war davon betroffen. 'description' bleibt bewusst unangetastet,
damit ein '#' in der Beschreibung erhalten bleibt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8f1bf037cd
commit
e9aeaf9e62
+78
-8
@@ -61,12 +61,21 @@ def _pvesh_binary():
|
||||
class Proxmox:
|
||||
"""Zugriff auf die Proxmox-API ueber das Kommando `pvesh`."""
|
||||
|
||||
def __init__(self, dry_run=False, task_timeout=900, command_timeout=60):
|
||||
def __init__(self, dry_run=False, task_timeout=900, command_timeout=60,
|
||||
retries=2, retry_delay=60):
|
||||
self.dry_run = dry_run
|
||||
self.task_timeout = task_timeout
|
||||
self.command_timeout = command_timeout
|
||||
self.retries = retries
|
||||
self.retry_delay = retry_delay
|
||||
self._inventory_cache = None
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config):
|
||||
globals_ = config.globals
|
||||
return cls(dry_run=globals_.dry_run, task_timeout=globals_.task_timeout,
|
||||
retries=globals_.retries, retry_delay=globals_.retry_delay)
|
||||
|
||||
# -- unterste Ebene ---------------------------------------------------
|
||||
|
||||
def _run(self, args, timeout=None):
|
||||
@@ -159,29 +168,73 @@ class Proxmox:
|
||||
if self.dry_run:
|
||||
log.info("[TESTLAUF] wuerde Snapshot anlegen: %s -> %s", guest.label, name)
|
||||
return None
|
||||
stdout, _ = self._run(args)
|
||||
return self._wait_task(guest.node, stdout, "Snapshot %s fuer %s" % (name, guest.label))
|
||||
what = "Snapshot %s fuer %s" % (name, guest.label)
|
||||
return self._retry(what, lambda: self._task(guest, args, what))
|
||||
|
||||
def delete_snapshot(self, guest, name):
|
||||
if self.dry_run:
|
||||
log.info("[TESTLAUF] wuerde Snapshot loeschen: %s -> %s", guest.label, name)
|
||||
return None
|
||||
stdout, _ = self._run(["delete", "%s/%s" % (self._base_path(guest), name)])
|
||||
return self._wait_task(guest.node, stdout,
|
||||
"Loeschen von %s bei %s" % (name, guest.label))
|
||||
args = ["delete", "%s/%s" % (self._base_path(guest), name)]
|
||||
what = "Loeschen von %s bei %s" % (name, guest.label)
|
||||
return self._retry(what, lambda: self._task(guest, args, what))
|
||||
|
||||
def _task(self, guest, args, what):
|
||||
"""Fuehrt eine Snapshot-Aktion aus und wartet, bis sie wirklich fertig ist.
|
||||
|
||||
Wichtig: hier gilt das lange Task-Zeitlimit, nicht das kurze fuer
|
||||
Lesezugriffe. Proxmox wartet beim Anlegen bis zu 60s auf den
|
||||
Storage-Lock; liefen wir vorher in unser eigenes Zeitlimit, wuerde die
|
||||
naechste VM losgeschickt, waehrend der Task noch laeuft - und genau das
|
||||
laesst dann alle folgenden Snapshots am cfs-Lock scheitern.
|
||||
"""
|
||||
stdout, _ = self._run(args, timeout=self.task_timeout)
|
||||
return self._wait_task(guest, stdout, what)
|
||||
|
||||
# -- Wiederholung bei belegten Sperren --------------------------------
|
||||
|
||||
# Solche Meldungen sind voruebergehend - da lohnt ein zweiter Versuch.
|
||||
_TRANSIENT = ("cfs-lock", "lock request timeout", "trying to acquire",
|
||||
"can't lock file", "got lock timeout", "unable to acquire lock",
|
||||
"resource temporarily unavailable", "storage is locked",
|
||||
"vm is locked", "ct is locked")
|
||||
|
||||
@classmethod
|
||||
def _is_transient(cls, message):
|
||||
text = str(message).lower()
|
||||
return any(marker in text for marker in cls._TRANSIENT)
|
||||
|
||||
def _retry(self, what, action):
|
||||
attempt = 0
|
||||
while True:
|
||||
try:
|
||||
return action()
|
||||
except ProxmoxError as exc:
|
||||
if attempt >= self.retries or not self._is_transient(exc):
|
||||
raise
|
||||
attempt += 1
|
||||
log.warning("%s: %s - Versuch %d von %d in %ds",
|
||||
what, exc, attempt + 1, self.retries + 1, self.retry_delay)
|
||||
time.sleep(self.retry_delay)
|
||||
|
||||
# -- Task-Verfolgung --------------------------------------------------
|
||||
|
||||
def _wait_task(self, node, output, what):
|
||||
def _wait_task(self, guest, output, what):
|
||||
"""pvesh liefert bei Snapshot-Aktionen eine UPID; darauf warten wir."""
|
||||
upid = self._extract_upid(output)
|
||||
if not upid:
|
||||
# Ohne UPID koennen wir den Task nicht verfolgen - dann warten wir
|
||||
# ersatzweise, bis der Gast nicht mehr gesperrt ist. Sonst wuerde
|
||||
# die naechste Aktion in eine noch laufende hineinlaufen.
|
||||
log.debug("%s: keine UPID in der Antwort von pvesh", what)
|
||||
self._wait_unlocked(guest)
|
||||
return None
|
||||
|
||||
deadline = time.time() + self.task_timeout
|
||||
delay = 0.5
|
||||
while True:
|
||||
status = self._json(["get", "/nodes/%s/tasks/%s/status" % (node, upid)]) or {}
|
||||
status = self._json(["get", "/nodes/%s/tasks/%s/status"
|
||||
% (guest.node, upid)]) or {}
|
||||
if status.get("status") == "stopped":
|
||||
exit_status = status.get("exitstatus") or "unbekannt"
|
||||
if exit_status != "OK":
|
||||
@@ -193,6 +246,23 @@ class Proxmox:
|
||||
time.sleep(delay)
|
||||
delay = min(delay * 1.5, 5.0)
|
||||
|
||||
def _wait_unlocked(self, guest, timeout=None):
|
||||
"""Wartet, bis der Gast keine laufende Sperre mehr hat."""
|
||||
deadline = time.time() + (timeout or self.task_timeout)
|
||||
path = "/nodes/%s/%s/%d/status/current" % (guest.node, guest.type, guest.vmid)
|
||||
while True:
|
||||
try:
|
||||
status = self._json(["get", path]) or {}
|
||||
except ProxmoxError:
|
||||
return # lieber weitermachen als haengen bleiben
|
||||
if not status.get("lock"):
|
||||
return
|
||||
if time.time() > deadline:
|
||||
raise ProxmoxError("%s ist seit %ds gesperrt (%s)"
|
||||
% (guest.label, timeout or self.task_timeout,
|
||||
status.get("lock")))
|
||||
time.sleep(2.0)
|
||||
|
||||
@staticmethod
|
||||
def _extract_upid(output):
|
||||
for line in (output or "").splitlines():
|
||||
|
||||
Reference in New Issue
Block a user