pvesnap-recovery: Snapshots als Maschine starten
Zwei Betriebsarten:
live abgeschottet ohne Netzwerk - zum Hineinschauen ueber die
noVNC-Konsole, Dump ziehen, Daten ueber ein Austauschlaufwerk
herausholen
recovery mit Netzwerk und gleicher Identitaet (SMBIOS-UUID, vmgenid,
MAC-Adressen, Hostname) - warnt, wenn das Original noch laeuft
Die Datentraeger werden ueber PVE::Storage::vdisk_clone geklont, also mit
derselben Funktion und Cluster-Sperre, die Proxmox selbst verwendet. Auf
Ceph/ZFS/LVM-thin ist das Copy-on-Write: 32 GiB waren im Test in 0,8s
geklont, das Original bleibt unberuehrt.
Enthaelt der Snapshot den Arbeitsspeicher, wird er mitkopiert - die Maschine
laeuft dann genau dort weiter, wo sie stand, statt zu booten. Dafuer muss die
Geraeteausstattung exakt passen:
- vmgenid und smbios1 bleiben erhalten (ohne vmgenid bricht das Laden mit
"Unknown savevm section" ab)
- runningmachine/runningcpu werden uebernommen
- die Netzwerkkarte wird abgeklemmt statt entfernt
- das Austauschlaufwerk kommt erst nach dem Fortsetzen per Hotplug dazu,
sonst bemerkt der Gast es nie (nachgewiesen ueber "info virtio-status")
Proxmox meldet einen fehlgeschlagenen RAM-Ladevorgang als TASK OK und laesst
die Maschine angehalten stehen - deshalb wird das Task-Protokoll mitgelesen,
fortgesetzt und deutlich gemeldet, wenn stattdessen kalt gebootet wurde.
Austausch mit dem Host: bei VMs eine formatierte Zusatzplatte, bei
Containern ein durchgereichtes Verzeichnis - dort passt auch ein bereits
eingehaengtes Netzlaufwerk.
Verworfen wird restlos, samt Aufheben des rbd-Snapshot-Schutzes, den das
Klonen setzt - sonst koennte die Vorhaltezeit den Snapshot nie mehr loeschen.
Angefasst wird nur, was den Tag pvesnap-recovery traegt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8577b23b0c
commit
725fbf8732
+105
-4
@@ -207,6 +207,103 @@ class Proxmox:
|
||||
what = "Loeschen von %s bei %s" % (name, guest.label)
|
||||
return self._retry(what, lambda: self._task(guest, args, what))
|
||||
|
||||
# -- Gaeste lesen und steuern -----------------------------------------
|
||||
|
||||
def guest_config(self, guest, snapshot=None):
|
||||
"""Konfiguration eines Gastes - wahlweise die zu einem Snapshot."""
|
||||
tail = ("snapshot/%s/config" % snapshot) if snapshot else "config"
|
||||
data = self._json(["get", "/nodes/%s/%s/%d/%s"
|
||||
% (guest.node, guest.type, guest.vmid, tail)])
|
||||
if not isinstance(data, dict):
|
||||
raise ProxmoxError("Konfiguration von %s nicht lesbar" % guest.label)
|
||||
return data
|
||||
|
||||
def guest_status(self, guest):
|
||||
try:
|
||||
data = self._json(["get", "/nodes/%s/%s/%d/status/current"
|
||||
% (guest.node, guest.type, guest.vmid)])
|
||||
except ProxmoxError:
|
||||
return {}
|
||||
return data if isinstance(data, dict) else {}
|
||||
|
||||
def guest_exists(self, vmid):
|
||||
return any(g.vmid == int(vmid) for g in self.inventory(refresh=True))
|
||||
|
||||
def next_vmid(self, wanted=None):
|
||||
"""Freie VMID. Mit `wanted` wird geprueft, ob genau die frei ist."""
|
||||
args = ["get", "/cluster/nextid"]
|
||||
if wanted:
|
||||
args += ["--vmid", str(int(wanted))]
|
||||
return int(self._json(args))
|
||||
|
||||
def vmid_free(self, vmid):
|
||||
try:
|
||||
self.next_vmid(vmid)
|
||||
return True
|
||||
except (ProxmoxError, TypeError, ValueError):
|
||||
return False
|
||||
|
||||
def start_guest(self, guest):
|
||||
return self._guest_task(guest, "create", "status/start",
|
||||
"Starten von %s" % guest.label)
|
||||
|
||||
def shutdown_guest(self, guest, timeout=180, force=True):
|
||||
"""Sauberes Herunterfahren; `force` schaltet nach Ablauf hart ab."""
|
||||
extra = ["--timeout", str(int(timeout))]
|
||||
if force:
|
||||
extra += ["--forceStop", "1"]
|
||||
return self._guest_task(guest, "create", "status/shutdown",
|
||||
"Herunterfahren von %s" % guest.label, extra,
|
||||
timeout=timeout + 60)
|
||||
|
||||
def stop_guest(self, guest):
|
||||
return self._guest_task(guest, "create", "status/stop",
|
||||
"Ausschalten von %s" % guest.label)
|
||||
|
||||
def set_guest_config(self, guest, values):
|
||||
"""Konfigurationswerte setzen. Laeuft der Gast, steckt Proxmox
|
||||
hotplug-faehige Geraete dabei gleich an."""
|
||||
args = ["set", "/nodes/%s/%s/%d/config" % (guest.node, guest.type, guest.vmid)]
|
||||
for key, value in values.items():
|
||||
args += ["--%s" % key, str(value)]
|
||||
if self.dry_run:
|
||||
log.info("[TESTLAUF] wuerde setzen: %s", " ".join(args))
|
||||
return None
|
||||
stdout, _ = self._run(args, timeout=self.task_timeout)
|
||||
return self._wait_upid(guest.node, stdout, "Aendern von %s" % guest.label,
|
||||
guest)
|
||||
|
||||
def resume_guest(self, guest):
|
||||
return self._guest_task(guest, "create", "status/resume",
|
||||
"Fortsetzen von %s" % guest.label)
|
||||
|
||||
def task_log(self, node, upid, limit=200):
|
||||
"""Das Protokoll eines Tasks als Liste von Zeilen."""
|
||||
try:
|
||||
entries = self._json(["get", "/nodes/%s/tasks/%s/log" % (node, upid),
|
||||
"--limit", str(int(limit))]) or []
|
||||
except ProxmoxError:
|
||||
return []
|
||||
return [str(entry.get("t") or "") for entry in entries]
|
||||
|
||||
def destroy_guest(self, guest, purge=True):
|
||||
extra = ["--purge", "1"] if purge else []
|
||||
if guest.type == "qemu":
|
||||
extra += ["--destroy-unreferenced-disks", "1"]
|
||||
return self._guest_task(guest, "delete", "", "Entfernen von %s" % guest.label,
|
||||
extra)
|
||||
|
||||
def _guest_task(self, guest, verb, sub, what, extra=None, timeout=None):
|
||||
path = "/nodes/%s/%s/%d" % (guest.node, guest.type, guest.vmid)
|
||||
if sub:
|
||||
path += "/" + sub
|
||||
args = [verb, path] + (extra or [])
|
||||
if self.dry_run:
|
||||
log.info("[TESTLAUF] wuerde ausfuehren: %s", " ".join(args))
|
||||
return None
|
||||
stdout, _ = self._run(args, timeout=timeout or self.task_timeout)
|
||||
return self._wait_upid(guest.node, stdout, what, guest)
|
||||
|
||||
def _task(self, guest, args, what):
|
||||
"""Fuehrt eine Snapshot-Aktion aus und wartet, bis sie wirklich fertig ist.
|
||||
|
||||
@@ -255,27 +352,31 @@ class Proxmox:
|
||||
# -- Task-Verfolgung --------------------------------------------------
|
||||
|
||||
def _wait_task(self, guest, output, what):
|
||||
"""pvesh liefert bei Snapshot-Aktionen eine UPID; darauf warten wir."""
|
||||
return self._wait_upid(guest.node, output, what, guest)
|
||||
|
||||
def _wait_upid(self, node, output, what, guest=None):
|
||||
"""pvesh liefert bei laengeren 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)
|
||||
if guest is not None:
|
||||
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"
|
||||
% (guest.node, upid)]) or {}
|
||||
% (node, upid)]) or {}
|
||||
if status.get("status") == "stopped":
|
||||
exit_status = status.get("exitstatus") or "unbekannt"
|
||||
if exit_status != "OK":
|
||||
# Nur die Ursache melden - wer und was, ergaenzt der Aufrufer.
|
||||
raise ProxmoxError("%s%s" % (exit_status,
|
||||
self._task_log_tail(guest.node, upid)))
|
||||
self._task_log_tail(node, upid)))
|
||||
return upid
|
||||
if time.time() > deadline:
|
||||
raise ProxmoxError("%s: Zeitueberschreitung nach %ds (Task laeuft weiter: %s)"
|
||||
|
||||
Reference in New Issue
Block a user