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)"
|
||||
|
||||
+1207
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,953 @@
|
||||
"""pvesnap-recovery - einen Snapshot als Maschine starten.
|
||||
|
||||
Ohne Argumente kommt die ncurses-Oberflaeche, alles geht aber auch direkt:
|
||||
|
||||
pvesnap-recovery Uebersicht und Assistent
|
||||
pvesnap-recovery list was gerade laeuft
|
||||
pvesnap-recovery live 802 neuesten Snapshot ohne Netz starten
|
||||
pvesnap-recovery live 802 <snapshot> --exchange 20G
|
||||
pvesnap-recovery recover 802 <snapshot> mit Netz, gleiche Identitaet
|
||||
pvesnap-recovery console 9802 noVNC-Adresse anzeigen
|
||||
pvesnap-recovery pull 9802 Austauschlaufwerk auf den Host holen
|
||||
pvesnap-recovery destroy 9802 restlos verwerfen
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import curses
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
from . import __version__
|
||||
from .curses_util import (C_DIM, C_FOOTER, C_HEADER, C_MARK, C_OK, C_SEL, C_WARN,
|
||||
choose, confirm, fill, init_colors, is_enter, is_escape,
|
||||
keybar, message, prompt, put, read_key)
|
||||
from .proxmox import Proxmox, ProxmoxError, pvesh_available
|
||||
from .recovery import (EXCHANGE_GUEST_PATH, MKFS, RecoveryError, Spec, create,
|
||||
destroy, human_bytes, list_instances, plan, pull, release,
|
||||
start)
|
||||
from .snapfs import list_snapshots
|
||||
from .util import truncate
|
||||
|
||||
log = logging.getLogger("pvesnap.recovery_ui")
|
||||
|
||||
KEYS = [
|
||||
("Enter", "Details", "Detail"),
|
||||
("n", "Neu", "Neu"),
|
||||
("s", "Starten", "Start"),
|
||||
("h", "Herunterfahren", "Stop"),
|
||||
("a", "Austausch", "Austau."),
|
||||
("x", "Verwerfen", "Verwerf."),
|
||||
("r", "Aktualisieren", "Aktual."),
|
||||
("q", "Beenden", "Ende"),
|
||||
]
|
||||
|
||||
NET_CHOICES = (
|
||||
("", "automatisch (nach Modus)"),
|
||||
("none", "keine Netzwerkkarte"),
|
||||
("down", "Karte vorhanden, abgeklemmt"),
|
||||
("on", "voll am Netz - Vorsicht!"),
|
||||
)
|
||||
|
||||
RESUME_CHOICES = (
|
||||
(None, "wenn im Snapshot vorhanden"),
|
||||
(True, "ja, Arbeitsspeicher laden"),
|
||||
(False, "nein, kalt starten"),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Kleine Bausteine
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _wait_screen(win, text, title="pvesnap-recovery"):
|
||||
height, width = win.getmaxyx()
|
||||
win.erase()
|
||||
fill(win, 0, " " + title, curses.color_pair(C_HEADER) | curses.A_BOLD)
|
||||
put(win, height // 2, max(2, (width - len(text)) // 2), text, curses.A_BOLD)
|
||||
win.refresh()
|
||||
|
||||
|
||||
def _pages(win, lines, title, footer="beliebige Taste = weiter"):
|
||||
"""Einen Textblock anzeigen, bei Bedarf seitenweise."""
|
||||
height, width = win.getmaxyx()
|
||||
rows = max(1, height - 4)
|
||||
top = 0
|
||||
while True:
|
||||
win.erase()
|
||||
fill(win, 0, " " + title, curses.color_pair(C_HEADER) | curses.A_BOLD)
|
||||
for row, (text, attr) in enumerate(lines[top:top + rows]):
|
||||
put(win, row + 2, 2, truncate(text, width - 4), attr)
|
||||
more = " (%d-%d von %d)" % (top + 1, min(top + rows, len(lines)), len(lines)) \
|
||||
if len(lines) > rows else ""
|
||||
fill(win, height - 1, " " + footer + more, curses.color_pair(C_FOOTER))
|
||||
win.refresh()
|
||||
key = read_key(win)
|
||||
if key == curses.KEY_DOWN and top + rows < len(lines):
|
||||
top += 1
|
||||
elif key == curses.KEY_UP and top > 0:
|
||||
top -= 1
|
||||
elif key == curses.KEY_NPAGE and top + rows < len(lines):
|
||||
top = min(len(lines) - rows, top + rows)
|
||||
elif key == curses.KEY_PPAGE:
|
||||
top = max(0, top - rows)
|
||||
else:
|
||||
return key
|
||||
|
||||
|
||||
def _plain(lines):
|
||||
return [(text, 0) for text in lines]
|
||||
|
||||
|
||||
class _Progress:
|
||||
"""Fortschrittsanzeige waehrend des Anlegens."""
|
||||
|
||||
def __init__(self, win, title):
|
||||
self.win = win
|
||||
self.title = title
|
||||
self.lines = []
|
||||
|
||||
def __call__(self, text):
|
||||
self.lines.append(text)
|
||||
height, width = self.win.getmaxyx()
|
||||
self.win.erase()
|
||||
fill(self.win, 0, " " + self.title, curses.color_pair(C_HEADER) | curses.A_BOLD)
|
||||
visible = self.lines[-(height - 4):]
|
||||
for row, line in enumerate(visible):
|
||||
attr = curses.A_BOLD if line is visible[-1] else curses.color_pair(C_DIM)
|
||||
put(self.win, row + 2, 2, truncate(line, width - 4), attr)
|
||||
fill(self.win, height - 1, " bitte warten ...", curses.color_pair(C_FOOTER))
|
||||
self.win.refresh()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Auswahl
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _pick_guest(win, guests):
|
||||
entries = [(g, "%s %-6d %-24s %-8s %s"
|
||||
% ("LXC" if g.type == "lxc" else "VM ", g.vmid,
|
||||
truncate(g.name or "-", 24), g.status, g.node))
|
||||
for g in guests]
|
||||
return choose(win, "Von welchem Gast? (Esc = zurueck)", entries)
|
||||
|
||||
|
||||
def _pick_snapshot(win, snapshots):
|
||||
entries = []
|
||||
for snap in snapshots:
|
||||
when = (datetime.fromtimestamp(snap.snaptime).strftime("%d.%m.%Y %H:%M")
|
||||
if snap.snaptime else "?")
|
||||
ram = "RAM" if "vmstate" in (snap.description or "") else ""
|
||||
entries.append((snap.name, "%-34s %s %-4s %s"
|
||||
% (truncate(snap.name, 34), when, ram,
|
||||
truncate((snap.description or "").replace("\n", " "), 34))))
|
||||
return choose(win, "Welcher Snapshot? (Esc = zurueck)", entries)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Optionsmaske
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _fields(guest):
|
||||
"""Die Eingabefelder - je nach Gasttyp etwas andere."""
|
||||
rows = [
|
||||
("mode", "Betriebsart", "choice",
|
||||
(("live", "live - abgeschottet, ohne Netzwerk"),
|
||||
("recovery", "recovery - mit Netz, gleiche Identitaet"))),
|
||||
("net", "Netzwerk", "choice", NET_CHOICES),
|
||||
]
|
||||
if guest.type == "qemu":
|
||||
rows.append(("resume", "Arbeitsspeicher", "choice", RESUME_CHOICES))
|
||||
rows.append(("exchange", "Austauschlaufwerk", "text", ()))
|
||||
rows.append(("exchange_fs", " Dateisystem", "choice",
|
||||
tuple((f, f) for f in sorted(MKFS))))
|
||||
else:
|
||||
rows.append(("exchange", "Austauschverzeichnis", "choice",
|
||||
(("", "keins"), ("bind", "ja, unter %s im Gast"
|
||||
% EXCHANGE_GUEST_PATH))))
|
||||
rows.append(("exchange_dir", " eigenes Host-Verzeichnis", "text", ()))
|
||||
rows += [
|
||||
("newid", "Neue VMID", "text", ()),
|
||||
("node", "Node", "text", ()),
|
||||
("start", "Danach starten", "choice", ((True, "ja"), (False, "nein"))),
|
||||
]
|
||||
return rows
|
||||
|
||||
|
||||
def _field_text(spec, key, kind, choices):
|
||||
value = getattr(spec, key)
|
||||
if kind == "choice":
|
||||
for candidate, label in choices:
|
||||
if candidate == value:
|
||||
return label
|
||||
return str(value)
|
||||
if key == "newid":
|
||||
return str(value) if value else "automatisch (naechste freie)"
|
||||
if key == "exchange":
|
||||
return value or "keins"
|
||||
if key == "node":
|
||||
return value or "wie das Original"
|
||||
if key == "exchange_dir":
|
||||
return value or "(eigenes anlegen)"
|
||||
return str(value or "")
|
||||
|
||||
|
||||
def _options(win, guest, spec):
|
||||
"""Maske fuer die Einstellungen. True = anlegen, False = abbrechen."""
|
||||
rows = _fields(guest)
|
||||
index = 0
|
||||
while True:
|
||||
height, width = win.getmaxyx()
|
||||
win.erase()
|
||||
fill(win, 0, " Wiederherstellung einrichten - %s" % guest.label,
|
||||
curses.color_pair(C_HEADER) | curses.A_BOLD)
|
||||
|
||||
for row, (key, label, kind, choices) in enumerate(rows):
|
||||
selected = row == index
|
||||
attr = curses.color_pair(C_SEL) if selected else 0
|
||||
text = " %-26s %s" % (label + ":", _field_text(spec, key, kind, choices))
|
||||
fill(win, row + 2, text.ljust(width - 2), attr)
|
||||
|
||||
note = {
|
||||
"mode": "live = zum Hineinschauen. recovery = die Maschine wirklich "
|
||||
"wieder in Betrieb nehmen.",
|
||||
"net": "Ohne Netz kommt nichts hinein und nichts hinaus - dafuer ist "
|
||||
"das Austauschlaufwerk da.",
|
||||
"resume": "Snapshots mit Arbeitsspeicher laufen genau dort weiter, wo "
|
||||
"sie standen - ohne Crash-Recovery.",
|
||||
"exchange": ("Groesse wie 20G. Leer = keins."
|
||||
if guest.type == "qemu" else
|
||||
"Ein Host-Verzeichnis, das im Container auftaucht."),
|
||||
"exchange_fs": "Dateisystem des Austauschlaufwerks.",
|
||||
"exchange_dir": "Leer = pvesnap legt selbst eines an. Hier passt auch "
|
||||
"ein eingehaengtes Netzlaufwerk.",
|
||||
"newid": "VMID der neuen Maschine.",
|
||||
"node": "Auf welchem Node sie laufen soll.",
|
||||
"start": "Sofort hochfahren.",
|
||||
}.get(rows[index][0], "")
|
||||
for offset, line in enumerate(_wrap(note, width - 6)[:3]):
|
||||
put(win, len(rows) + 4 + offset, 3, line, curses.color_pair(C_DIM))
|
||||
|
||||
for offset, line in enumerate(keybar(
|
||||
width, [("Pfeile", "Auswaehlen", "Wahl"),
|
||||
("Enter/Leer", "Aendern", "Aendern"),
|
||||
("F10", "Anlegen", "Anlegen"),
|
||||
("q", "Abbrechen", "Abbr.")])):
|
||||
fill(win, height - 2 + offset, " " + line, curses.color_pair(C_FOOTER))
|
||||
win.refresh()
|
||||
|
||||
key = read_key(win)
|
||||
current_key, _label, kind, choices = rows[index]
|
||||
if key == curses.KEY_UP:
|
||||
index = (index - 1) % len(rows)
|
||||
elif key == curses.KEY_DOWN:
|
||||
index = (index + 1) % len(rows)
|
||||
elif key in (curses.KEY_F10,) or key == "\x06":
|
||||
return True
|
||||
elif is_escape(key) or key == "q":
|
||||
return False
|
||||
elif is_enter(key) or key == " " or key in (curses.KEY_RIGHT, curses.KEY_LEFT):
|
||||
_edit(win, spec, current_key, kind, choices,
|
||||
back=key == curses.KEY_LEFT)
|
||||
|
||||
|
||||
def _wrap(text, width):
|
||||
words, lines, current = str(text).split(), [], ""
|
||||
for word in words:
|
||||
if len(current) + len(word) + 1 > width:
|
||||
lines.append(current)
|
||||
current = word
|
||||
else:
|
||||
current = (current + " " + word).strip()
|
||||
if current:
|
||||
lines.append(current)
|
||||
return lines
|
||||
|
||||
|
||||
def _edit(win, spec, key, kind, choices, back=False):
|
||||
if kind == "choice":
|
||||
values = [c[0] for c in choices]
|
||||
try:
|
||||
position = values.index(getattr(spec, key))
|
||||
except ValueError:
|
||||
position = 0
|
||||
position = (position - 1 if back else position + 1) % len(values)
|
||||
setattr(spec, key, values[position])
|
||||
if key == "mode":
|
||||
# Die Betriebsart gibt die Vorgabe fuer das Netz vor.
|
||||
spec.net = ""
|
||||
return
|
||||
|
||||
label = {"exchange": "Groesse (z.B. 20G, leer = keins)",
|
||||
"newid": "Neue VMID (leer = automatisch)",
|
||||
"node": "Node (leer = wie das Original)",
|
||||
"exchange_dir": "Host-Verzeichnis (leer = selbst anlegen)"}.get(key, key)
|
||||
answer = prompt(win, label, str(getattr(spec, key) or ""))
|
||||
if answer is None:
|
||||
return
|
||||
if key == "newid":
|
||||
spec.newid = int(answer) if answer.isdigit() else 0
|
||||
else:
|
||||
setattr(spec, key, answer)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Zusammenfassung
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def summary_lines(plan_):
|
||||
"""Was passieren wird - als Liste von (Text, Attribut)."""
|
||||
guest, spec = plan_.guest, plan_.spec
|
||||
lines = [
|
||||
("Quelle: %s auf %s" % (guest.label, guest.node), 0),
|
||||
("Snapshot: %s" % plan_.snapshot, 0),
|
||||
("Neu: %s auf %s" % (plan_.label, plan_.node), curses.A_BOLD),
|
||||
("Betriebsart: %s" % ("recovery - mit Netz, gleiche Identitaet"
|
||||
if spec.mode == "recovery"
|
||||
else "live - abgeschottet"), 0),
|
||||
("", 0),
|
||||
("Datentraeger (Copy-on-Write-Klone, das Original bleibt unberuehrt):", 0),
|
||||
]
|
||||
for key, volid, size in plan_.disks:
|
||||
lines.append((" %-9s %s%s" % (key, volid, (" %s" % size) if size else ""), 0))
|
||||
|
||||
lines.append(("", 0))
|
||||
netzwerk = {"none": "keine Netzwerkkarte",
|
||||
"down": "Karte vorhanden, Leitung abgeklemmt",
|
||||
"on": "AM NETZ - mit den MAC-Adressen des Originals"}
|
||||
lines.append(("Netzwerk: %s" % netzwerk.get(plan_.net_mode, plan_.net_mode),
|
||||
curses.color_pair(C_WARN) if plan_.net_mode == "on" else 0))
|
||||
if plan_.nets:
|
||||
for key, value in plan_.nets:
|
||||
lines.append((" %-9s %s" % (key, truncate(value, 60)), 0))
|
||||
|
||||
if plan_.vmstate:
|
||||
lines.append(("Arbeitsspeicher: %s"
|
||||
% ("wird geladen (%s) - laeuft weiter statt zu booten"
|
||||
% (human_bytes(plan_.vmstate_bytes) or "?")
|
||||
if plan_.resume else "vorhanden, wird aber nicht geladen"),
|
||||
curses.color_pair(C_OK) if plan_.resume else 0))
|
||||
if plan_.exchange_kind:
|
||||
lines.append(("Austausch: %s" % plan_.exchange_detail, 0))
|
||||
|
||||
if plan_.notes:
|
||||
lines.append(("", 0))
|
||||
for note in plan_.notes:
|
||||
for line in _wrap("- " + note, 74):
|
||||
lines.append((line, curses.color_pair(C_DIM)))
|
||||
if plan_.warnings:
|
||||
lines.append(("", 0))
|
||||
for warning in plan_.warnings:
|
||||
for line in _wrap("!! " + warning, 74):
|
||||
lines.append((line, curses.color_pair(C_WARN) | curses.A_BOLD))
|
||||
return lines
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Assistent
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _new(stdscr, proxmox):
|
||||
_wait_screen(stdscr, "Frage Proxmox nach VMs und Containern ...")
|
||||
try:
|
||||
guests = proxmox.inventory(refresh=True)
|
||||
except ProxmoxError as exc:
|
||||
message(stdscr, str(exc), error=True)
|
||||
return
|
||||
|
||||
while True:
|
||||
guest = _pick_guest(stdscr, guests)
|
||||
if guest is None:
|
||||
return
|
||||
|
||||
_wait_screen(stdscr, "Lese Snapshots von %s ..." % guest.label)
|
||||
try:
|
||||
snapshots = [s for s in list_snapshots(proxmox, guest) if s.complete]
|
||||
except ProxmoxError as exc:
|
||||
message(stdscr, str(exc), error=True)
|
||||
continue
|
||||
if not snapshots:
|
||||
message(stdscr, "%s hat keine brauchbaren Snapshots." % guest.label,
|
||||
error=True)
|
||||
continue
|
||||
|
||||
snapname = _pick_snapshot(stdscr, snapshots)
|
||||
if snapname is None:
|
||||
continue
|
||||
|
||||
spec = Spec(exchange_dir="")
|
||||
while True:
|
||||
if not _options(stdscr, guest, spec):
|
||||
break
|
||||
_wait_screen(stdscr, "Pruefe, was noetig ist ...")
|
||||
try:
|
||||
plan_ = plan(proxmox, guest, snapname, spec)
|
||||
except (RecoveryError, ProxmoxError) as exc:
|
||||
_pages(stdscr, [(line, curses.color_pair(C_WARN))
|
||||
for line in _wrap(str(exc), 74)],
|
||||
"Geht so nicht")
|
||||
continue
|
||||
|
||||
key = _pages(stdscr, summary_lines(plan_),
|
||||
"Zusammenfassung - noch ist nichts passiert",
|
||||
"j = anlegen | beliebige andere Taste = zurueck")
|
||||
if key not in ("j", "J", "y", "Y"):
|
||||
continue
|
||||
if plan_.warnings and not confirm(
|
||||
stdscr, "Warnungen gelesen und trotzdem anlegen?"):
|
||||
continue
|
||||
|
||||
progress = _Progress(stdscr, "Richte %s ein" % plan_.label)
|
||||
try:
|
||||
instance = create(proxmox, plan_, progress=progress)
|
||||
except (RecoveryError, ProxmoxError, OSError) as exc:
|
||||
_pages(stdscr, [(line, curses.color_pair(C_WARN))
|
||||
for line in _wrap(str(exc), 74)]
|
||||
+ [("", 0)] + _plain(progress.lines),
|
||||
"Fehlgeschlagen")
|
||||
break
|
||||
|
||||
if spec.start:
|
||||
progress("Starte %s ..." % instance.label)
|
||||
try:
|
||||
hint = start(proxmox, instance, progress=progress)
|
||||
except (ProxmoxError, RecoveryError) as exc:
|
||||
message(stdscr, "Angelegt, aber nicht gestartet: %s" % exc,
|
||||
error=True)
|
||||
else:
|
||||
if hint:
|
||||
_pages(stdscr, [(line, curses.color_pair(C_WARN))
|
||||
for line in _wrap(hint, 74)],
|
||||
"Angelegt und gestartet - mit einem Hinweis")
|
||||
_details(stdscr, proxmox, instance)
|
||||
return
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Detailansicht
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _details(stdscr, proxmox, instance):
|
||||
while True:
|
||||
status = proxmox.guest_status(instance.guest)
|
||||
running = status.get("status") == "running"
|
||||
lines = [
|
||||
("Maschine: %s auf %s" % (instance.label, instance.node), curses.A_BOLD),
|
||||
("Zustand: %s" % (status.get("status") or "unbekannt"),
|
||||
curses.color_pair(C_OK if running else C_DIM)),
|
||||
("Herkunft: %s" % instance.origin, 0),
|
||||
("Betriebsart: %s" % instance.mode, 0),
|
||||
("Angelegt: %s" % (datetime.fromtimestamp(instance.created)
|
||||
.strftime("%d.%m.%Y %H:%M")
|
||||
if instance.created else "?"), 0),
|
||||
("", 0),
|
||||
("Konsole im Browser:", 0),
|
||||
]
|
||||
for line in _wrap(instance.console_url(), 74):
|
||||
lines.append((" " + line, curses.color_pair(C_MARK)))
|
||||
if instance.type == "lxc":
|
||||
lines.append((" oder auf dem Host: pct enter %d" % instance.vmid,
|
||||
curses.color_pair(C_MARK)))
|
||||
|
||||
exchange = instance.exchange or {}
|
||||
if exchange:
|
||||
lines.append(("", 0))
|
||||
if exchange.get("kind") == "bind":
|
||||
lines.append(("Austausch: %s" % exchange.get("path"), 0))
|
||||
lines.append((" im Gast unter %s" % EXCHANGE_GUEST_PATH, 0))
|
||||
else:
|
||||
lines.append(("Austausch: %s (%s), im Gast als weitere Platte"
|
||||
% (exchange.get("volid"), exchange.get("fs")), 0))
|
||||
lines.append((" nach dem Herunterfahren mit 'a' "
|
||||
"auf den Host holen", curses.color_pair(C_DIM)))
|
||||
if instance.resumed:
|
||||
lines.append(("", 0))
|
||||
lines.append(("Der Arbeitsspeicher des Snapshots wurde geladen - beim "
|
||||
"ersten Start lief die Maschine genau weiter.",
|
||||
curses.color_pair(C_DIM)))
|
||||
|
||||
footer = ("s Starten | h Herunterfahren | a Austausch | x Verwerfen | "
|
||||
"q zurueck")
|
||||
key = _pages(stdscr, lines, "Wiederherstellung %s" % instance.label, footer)
|
||||
|
||||
if key in ("q", "\x1b") or is_escape(key):
|
||||
return
|
||||
if key in ("s", "S"):
|
||||
_do(stdscr, "Starte %s ..." % instance.label,
|
||||
lambda: start(proxmox, instance))
|
||||
elif key in ("h", "H"):
|
||||
_do(stdscr, "Fahre %s herunter ..." % instance.label,
|
||||
lambda: proxmox.shutdown_guest(instance.guest))
|
||||
elif key in ("a", "A"):
|
||||
_pull(stdscr, proxmox, instance)
|
||||
elif key in ("x", "X"):
|
||||
if _destroy(stdscr, proxmox, instance):
|
||||
return
|
||||
|
||||
|
||||
def _do(stdscr, text, action):
|
||||
_wait_screen(stdscr, text)
|
||||
try:
|
||||
hint = action()
|
||||
except (ProxmoxError, RecoveryError) as exc:
|
||||
message(stdscr, str(exc), error=True)
|
||||
return
|
||||
if isinstance(hint, str) and hint:
|
||||
message(stdscr, hint, error=True)
|
||||
|
||||
|
||||
def _pull(stdscr, proxmox, instance):
|
||||
_wait_screen(stdscr, "Hole das Austauschlaufwerk auf den Host ...")
|
||||
try:
|
||||
path = pull(proxmox, instance)
|
||||
except (RecoveryError, ProxmoxError, OSError) as exc:
|
||||
message(stdscr, str(exc), error=True)
|
||||
return
|
||||
_pages(stdscr, _plain([
|
||||
"Das Austauschlaufwerk liegt jetzt hier:",
|
||||
"",
|
||||
" " + path,
|
||||
"",
|
||||
"Von dort kopieren, zum Beispiel mit:",
|
||||
" pvesnap-explorer --path %s" % path,
|
||||
" scp -r %s/. ziel:/pfad/" % path,
|
||||
"",
|
||||
"Wieder loesen mit: pvesnap-recovery release %d" % instance.vmid,
|
||||
]), "Austauschlaufwerk")
|
||||
|
||||
|
||||
def _destroy(stdscr, proxmox, instance):
|
||||
if not confirm(stdscr, "%s wirklich verwerfen? Alle Aenderungen darin sind weg."
|
||||
% instance.label):
|
||||
return False
|
||||
progress = _Progress(stdscr, "Verwerfe %s" % instance.label)
|
||||
try:
|
||||
destroy(proxmox, instance, progress=progress)
|
||||
except (RecoveryError, ProxmoxError) as exc:
|
||||
_pages(stdscr, [(line, curses.color_pair(C_WARN))
|
||||
for line in _wrap(str(exc), 74)] + [("", 0)]
|
||||
+ _plain(progress.lines), "Fehlgeschlagen")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Uebersicht
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _overview(stdscr, proxmox):
|
||||
index = 0
|
||||
instances = []
|
||||
reload_ = True
|
||||
while True:
|
||||
if reload_:
|
||||
_wait_screen(stdscr, "Frage nach vorhandenen Wiederherstellungen ...")
|
||||
instances = list_instances(proxmox)
|
||||
reload_ = False
|
||||
index = min(index, max(0, len(instances) - 1))
|
||||
|
||||
height, width = stdscr.getmaxyx()
|
||||
stdscr.erase()
|
||||
fill(stdscr, 0, " pvesnap-recovery %s - Snapshots als Maschine starten"
|
||||
% __version__, curses.color_pair(C_HEADER) | curses.A_BOLD)
|
||||
|
||||
if not instances:
|
||||
put(stdscr, 3, 3, "Zurzeit laeuft keine Wiederherstellung.", curses.A_BOLD)
|
||||
put(stdscr, 5, 3, "Mit n eine neue anlegen:", 0)
|
||||
put(stdscr, 6, 5, "live Snapshot abgeschottet starten, ohne Netzwerk -",
|
||||
curses.color_pair(C_DIM))
|
||||
put(stdscr, 7, 5, " zum Hineinschauen und Daten herausholen.",
|
||||
curses.color_pair(C_DIM))
|
||||
put(stdscr, 8, 5, "recovery Snapshot mit Netzwerk und gleicher Identitaet",
|
||||
curses.color_pair(C_DIM))
|
||||
put(stdscr, 9, 5, " wieder in Betrieb nehmen.",
|
||||
curses.color_pair(C_DIM))
|
||||
else:
|
||||
put(stdscr, 2, 2, "%-10s %-9s %-22s %-28s %s"
|
||||
% ("Maschine", "Zustand", "Node", "Herkunft", "Modus"),
|
||||
curses.A_BOLD | curses.A_UNDERLINE)
|
||||
rows = max(1, height - 6)
|
||||
top = max(0, min(index - rows + 1, len(instances) - rows))
|
||||
for row in range(min(rows, len(instances) - top)):
|
||||
instance = instances[top + row]
|
||||
attr = curses.color_pair(C_SEL) if top + row == index else 0
|
||||
if not attr and instance.status == "running":
|
||||
attr = curses.color_pair(C_OK)
|
||||
text = " %-10s %-9s %-22s %-28s %s" % (
|
||||
instance.label, instance.status or "?",
|
||||
truncate(instance.node or "?", 22),
|
||||
truncate(instance.origin, 28), instance.mode)
|
||||
fill(stdscr, row + 3, text, attr)
|
||||
|
||||
for offset, line in enumerate(keybar(width, KEYS)):
|
||||
fill(stdscr, height - 2 + offset, " " + line, curses.color_pair(C_FOOTER))
|
||||
stdscr.refresh()
|
||||
|
||||
key = read_key(stdscr)
|
||||
current = instances[index] if instances else None
|
||||
|
||||
if key in ("q", "Q") or is_escape(key):
|
||||
return 0
|
||||
if key in ("n", "N"):
|
||||
_new(stdscr, proxmox)
|
||||
reload_ = True
|
||||
elif key in ("r", "R"):
|
||||
reload_ = True
|
||||
elif key == curses.KEY_UP and instances:
|
||||
index = (index - 1) % len(instances)
|
||||
elif key == curses.KEY_DOWN and instances:
|
||||
index = (index + 1) % len(instances)
|
||||
elif current is None:
|
||||
continue
|
||||
elif is_enter(key):
|
||||
_details(stdscr, proxmox, current)
|
||||
reload_ = True
|
||||
elif key in ("s", "S"):
|
||||
_do(stdscr, "Starte %s ..." % current.label,
|
||||
lambda: start(proxmox, current))
|
||||
reload_ = True
|
||||
elif key in ("h", "H"):
|
||||
_do(stdscr, "Fahre %s herunter ..." % current.label,
|
||||
lambda: proxmox.shutdown_guest(current.guest))
|
||||
reload_ = True
|
||||
elif key in ("a", "A"):
|
||||
_pull(stdscr, proxmox, current)
|
||||
elif key in ("x", "X"):
|
||||
_destroy(stdscr, proxmox, current)
|
||||
reload_ = True
|
||||
|
||||
|
||||
def _main(stdscr, _args):
|
||||
curses.curs_set(0)
|
||||
if hasattr(curses, "set_escdelay"):
|
||||
curses.set_escdelay(25)
|
||||
init_colors()
|
||||
stdscr.keypad(True)
|
||||
return _overview(stdscr, Proxmox())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Befehlszeile
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _find_guest(proxmox, vmid):
|
||||
guest = next((g for g in proxmox.inventory() if g.vmid == int(vmid)), None)
|
||||
if guest is None:
|
||||
raise RecoveryError("Gast %s gibt es nicht." % vmid)
|
||||
return guest
|
||||
|
||||
|
||||
def _find_instance(proxmox, vmid):
|
||||
instance = next((i for i in list_instances(proxmox) if i.vmid == int(vmid)), None)
|
||||
if instance is None:
|
||||
raise RecoveryError("%s ist keine von pvesnap angelegte Wiederherstellung. "
|
||||
"Vorhandene zeigt: pvesnap-recovery list" % vmid)
|
||||
return instance
|
||||
|
||||
|
||||
def _newest_snapshot(proxmox, guest):
|
||||
snapshots = [s for s in list_snapshots(proxmox, guest) if s.complete]
|
||||
if not snapshots:
|
||||
raise RecoveryError("%s hat keine brauchbaren Snapshots." % guest.label)
|
||||
return snapshots[0].name
|
||||
|
||||
|
||||
def _spec_from_args(args, mode):
|
||||
spec = Spec(mode=mode)
|
||||
spec.newid = args.newid or 0
|
||||
spec.node = args.node or ""
|
||||
spec.net = args.net or ""
|
||||
spec.resume = args.resume
|
||||
spec.exchange = args.exchange or ""
|
||||
spec.exchange_storage = args.exchange_storage or ""
|
||||
spec.exchange_fs = args.exchange_fs
|
||||
spec.exchange_dir = args.exchange_dir or ""
|
||||
spec.memory = args.memory or 0
|
||||
spec.cores = args.cores or 0
|
||||
spec.name = args.name or ""
|
||||
spec.keep_binds = args.keep_binds
|
||||
spec.start = not args.no_start
|
||||
if spec.exchange_dir and not spec.exchange:
|
||||
spec.exchange = "bind"
|
||||
return spec
|
||||
|
||||
|
||||
def _print_summary(plan_):
|
||||
print(" Quelle : %s auf %s" % (plan_.guest.label, plan_.guest.node))
|
||||
print(" Snapshot : %s" % plan_.snapshot)
|
||||
print(" Neu : %s auf %s" % (plan_.label, plan_.node))
|
||||
for key, volid, size in plan_.disks:
|
||||
print(" Klon : %-9s %s%s" % (key, volid, (" %s" % size) if size else ""))
|
||||
netzwerk = {"none": "keine Netzwerkkarte",
|
||||
"down": "Karte vorhanden, Leitung abgeklemmt",
|
||||
"on": "AM NETZ mit den MAC-Adressen des Originals"}
|
||||
print(" Netzwerk : %s" % netzwerk.get(plan_.net_mode, plan_.net_mode))
|
||||
if plan_.resume:
|
||||
print(" Arbeitsspei.: wird geladen (%s)"
|
||||
% (human_bytes(plan_.vmstate_bytes) or "?"))
|
||||
if plan_.exchange_kind:
|
||||
print(" Austausch : %s" % plan_.exchange_detail)
|
||||
for note in plan_.notes:
|
||||
print(" Hinweis : %s" % note)
|
||||
for warning in plan_.warnings:
|
||||
print(" ACHTUNG : %s" % warning)
|
||||
|
||||
|
||||
def cmd_create(proxmox, args, mode):
|
||||
guest = _find_guest(proxmox, args.vmid)
|
||||
snapname = args.snapshot or _newest_snapshot(proxmox, guest)
|
||||
spec = _spec_from_args(args, mode)
|
||||
plan_ = plan(proxmox, guest, snapname, spec)
|
||||
|
||||
print("Vorhaben:")
|
||||
_print_summary(plan_)
|
||||
if not args.yes:
|
||||
if plan_.warnings:
|
||||
print("\nEs gibt Warnungen - bitte oben lesen.")
|
||||
answer = input("\nAnlegen? [j/N] ").strip().lower()
|
||||
if answer not in ("j", "y", "ja", "yes"):
|
||||
print("Abgebrochen.")
|
||||
return 1
|
||||
|
||||
instance = create(proxmox, plan_, progress=lambda text: print(" " + text))
|
||||
print("\n%s ist eingerichtet." % instance.label)
|
||||
if spec.start:
|
||||
print(" Starte ...")
|
||||
hint = start(proxmox, instance, progress=lambda text: print(" " + text))
|
||||
print(" laeuft." if not hint else " ACHTUNG: %s" % hint)
|
||||
_print_access(instance)
|
||||
return 0
|
||||
|
||||
|
||||
def _print_access(instance):
|
||||
print("\n Konsole im Browser:")
|
||||
print(" %s" % instance.console_url())
|
||||
if instance.type == "lxc":
|
||||
print(" oder auf dem Host: pct enter %d" % instance.vmid)
|
||||
exchange = instance.exchange or {}
|
||||
if exchange.get("kind") == "bind":
|
||||
print("\n Austausch: %s (im Gast unter %s)"
|
||||
% (exchange.get("path"), EXCHANGE_GUEST_PATH))
|
||||
elif exchange.get("volid"):
|
||||
print("\n Austausch: als weitere Platte im Gast (%s, Bezeichnung PVESNAP)"
|
||||
% exchange.get("fs"))
|
||||
print(" im Gast einhaengen, befuellen, Gast herunterfahren, dann:")
|
||||
print(" pvesnap-recovery pull %d" % instance.vmid)
|
||||
print("\n Wieder verwerfen: pvesnap-recovery destroy %d" % instance.vmid)
|
||||
|
||||
|
||||
def cmd_list(proxmox, _args):
|
||||
instances = list_instances(proxmox)
|
||||
if not instances:
|
||||
print("Zurzeit laeuft keine Wiederherstellung.")
|
||||
return 0
|
||||
print("%-10s %-9s %-14s %-30s %s"
|
||||
% ("Maschine", "Zustand", "Node", "Herkunft", "Modus"))
|
||||
for instance in instances:
|
||||
print("%-10s %-9s %-14s %-30s %s"
|
||||
% (instance.label, instance.status or "?", instance.node or "?",
|
||||
instance.origin, instance.mode))
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_destroy(proxmox, args):
|
||||
instance = _find_instance(proxmox, args.vmid)
|
||||
if not args.yes:
|
||||
print("%s (%s) wird restlos entfernt - alle Aenderungen darin sind weg."
|
||||
% (instance.label, instance.origin))
|
||||
if input("Wirklich? [j/N] ").strip().lower() not in ("j", "y", "ja", "yes"):
|
||||
print("Abgebrochen.")
|
||||
return 1
|
||||
destroy(proxmox, instance, progress=lambda text: print(" " + text))
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_pull(proxmox, args):
|
||||
instance = _find_instance(proxmox, args.vmid)
|
||||
path = pull(proxmox, instance)
|
||||
print(path)
|
||||
if not args.quiet:
|
||||
print("\nDas Austauschlaufwerk liegt jetzt auf dem Host.", file=sys.stderr)
|
||||
print(" pvesnap-explorer --path %s" % path, file=sys.stderr)
|
||||
print(" Wieder loesen: pvesnap-recovery release %d" % instance.vmid,
|
||||
file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_release(_proxmox, args):
|
||||
print("geloest." if release(args.vmid) else "War nicht eingehaengt.")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_console(proxmox, args):
|
||||
instance = _find_instance(proxmox, args.vmid)
|
||||
print(instance.console_url())
|
||||
if instance.type == "lxc":
|
||||
print("pct enter %d" % instance.vmid, file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_start(proxmox, args):
|
||||
hint = start(proxmox, _find_instance(proxmox, args.vmid),
|
||||
progress=lambda text: print(" " + text))
|
||||
print("laeuft." if not hint else "ACHTUNG: %s" % hint)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_stop(proxmox, args):
|
||||
proxmox.shutdown_guest(_find_instance(proxmox, args.vmid).guest)
|
||||
print("heruntergefahren.")
|
||||
return 0
|
||||
|
||||
|
||||
def _add_create_options(parser):
|
||||
parser.add_argument("vmid", type=int, help="VMID des Originals")
|
||||
parser.add_argument("snapshot", nargs="?",
|
||||
help="Name des Snapshots (Vorgabe: der neueste)")
|
||||
parser.add_argument("--newid", type=int, help="VMID der neuen Maschine")
|
||||
parser.add_argument("--node", help="Node, auf dem sie laufen soll")
|
||||
parser.add_argument("--net", choices=("none", "down", "on"),
|
||||
help="none = keine Karte, down = Karte ohne Leitung, "
|
||||
"on = voll am Netz")
|
||||
parser.add_argument("--resume", dest="resume", action="store_true", default=None,
|
||||
help="Arbeitsspeicher des Snapshots laden")
|
||||
parser.add_argument("--no-resume", dest="resume", action="store_false",
|
||||
help="kalt starten, auch wenn der Snapshot RAM enthaelt")
|
||||
parser.add_argument("--exchange", metavar="GROESSE",
|
||||
help="Austauschlaufwerk anlegen, z.B. 20G "
|
||||
"(bei Containern genuegt --exchange ja)")
|
||||
parser.add_argument("--exchange-storage", metavar="STORAGE",
|
||||
help="Storage dafuer (Vorgabe: wie die erste Platte)")
|
||||
parser.add_argument("--exchange-fs", default="ext4", choices=sorted(MKFS),
|
||||
help="Dateisystem des Austauschlaufwerks (Vorgabe: ext4)")
|
||||
parser.add_argument("--exchange-dir", metavar="PFAD",
|
||||
help="nur Container: dieses Host-Verzeichnis durchreichen "
|
||||
"(auch ein eingehaengtes Netzlaufwerk)")
|
||||
parser.add_argument("--memory", type=int, help="Arbeitsspeicher in MB abweichend")
|
||||
parser.add_argument("--cores", type=int, help="Kerne abweichend")
|
||||
parser.add_argument("--name", help="Name der neuen Maschine")
|
||||
parser.add_argument("--keep-binds", action="store_true",
|
||||
help="durchgereichte Host-Verzeichnisse des Originals "
|
||||
"uebernehmen (Vorgabe: weglassen)")
|
||||
parser.add_argument("--no-start", action="store_true",
|
||||
help="nur einrichten, nicht starten")
|
||||
parser.add_argument("-y", "--yes", action="store_true", help="nicht nachfragen")
|
||||
|
||||
|
||||
def build_parser():
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="pvesnap-recovery",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
description="Einen Proxmox-Snapshot als eigenstaendige Maschine starten.",
|
||||
epilog="""Beispiele:
|
||||
pvesnap-recovery
|
||||
ncurses-Oberflaeche mit Uebersicht und Assistent
|
||||
|
||||
pvesnap-recovery live 802 --exchange 20G
|
||||
neuesten Snapshot von VM 802 ohne Netzwerk starten, mit einem 20-GB-
|
||||
Austauschlaufwerk fuer den Datenbank-Dump
|
||||
|
||||
pvesnap-recovery live 100 --exchange ja
|
||||
Container 100 abgeschottet starten; ein Host-Verzeichnis erscheint
|
||||
im Container unter %s
|
||||
|
||||
pvesnap-recovery live 100 --exchange-dir /mnt/nfs/dumps
|
||||
dasselbe, aber mit einem eigenen (auch eingehaengten) Verzeichnis
|
||||
|
||||
pvesnap-recovery recover 802 autosnap-... --newid 9802
|
||||
mit Netzwerk und gleicher Identitaet - nur wenn das Original steht!
|
||||
|
||||
pvesnap-recovery pull 9802
|
||||
Austauschlaufwerk nach dem Herunterfahren auf den Host holen
|
||||
|
||||
pvesnap-recovery destroy 9802
|
||||
restlos verwerfen: Maschine weg, Klone weg, Schutz der Snapshots geloest
|
||||
""" % EXCHANGE_GUEST_PATH)
|
||||
parser.add_argument("-v", "--verbose", action="store_true")
|
||||
parser.add_argument("-V", "--version", action="version",
|
||||
version="pvesnap-recovery %s" % __version__)
|
||||
|
||||
subs = parser.add_subparsers(dest="command", metavar="BEFEHL")
|
||||
|
||||
live = subs.add_parser("live", help="Snapshot abgeschottet starten (ohne Netz)")
|
||||
_add_create_options(live)
|
||||
|
||||
recover = subs.add_parser("recover",
|
||||
help="Snapshot mit Netz und gleicher Identitaet starten")
|
||||
_add_create_options(recover)
|
||||
|
||||
subs.add_parser("list", help="vorhandene Wiederherstellungen zeigen")
|
||||
|
||||
for name, helptext in (("start", "hochfahren"), ("stop", "herunterfahren"),
|
||||
("console", "noVNC-Adresse zeigen")):
|
||||
sub = subs.add_parser(name, help=helptext)
|
||||
sub.add_argument("vmid", type=int)
|
||||
|
||||
pull_ = subs.add_parser("pull", help="Austauschlaufwerk auf den Host holen")
|
||||
pull_.add_argument("vmid", type=int)
|
||||
pull_.add_argument("-q", "--quiet", action="store_true",
|
||||
help="nur den Pfad ausgeben")
|
||||
|
||||
release_ = subs.add_parser("release", help="Austauschlaufwerk wieder loesen")
|
||||
release_.add_argument("vmid", type=int)
|
||||
|
||||
destroy_ = subs.add_parser("destroy", help="Wiederherstellung restlos verwerfen")
|
||||
destroy_.add_argument("vmid", type=int)
|
||||
destroy_.add_argument("-y", "--yes", action="store_true", help="nicht nachfragen")
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
args = build_parser().parse_args(argv)
|
||||
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.WARNING,
|
||||
format="%(levelname)s %(message)s")
|
||||
|
||||
if not pvesh_available():
|
||||
print("'pvesh' nicht gefunden - pvesnap-recovery laeuft auf dem "
|
||||
"Proxmox-VE-Host.", file=sys.stderr)
|
||||
return 3
|
||||
if os.geteuid() != 0:
|
||||
print("pvesnap-recovery braucht root-Rechte (Klone anlegen, Maschinen "
|
||||
"einrichten).", file=sys.stderr)
|
||||
return 4
|
||||
|
||||
proxmox = Proxmox()
|
||||
handlers = {
|
||||
"live": lambda: cmd_create(proxmox, args, "live"),
|
||||
"recover": lambda: cmd_create(proxmox, args, "recovery"),
|
||||
"list": lambda: cmd_list(proxmox, args),
|
||||
"destroy": lambda: cmd_destroy(proxmox, args),
|
||||
"pull": lambda: cmd_pull(proxmox, args),
|
||||
"release": lambda: cmd_release(proxmox, args),
|
||||
"console": lambda: cmd_console(proxmox, args),
|
||||
"start": lambda: cmd_start(proxmox, args),
|
||||
"stop": lambda: cmd_stop(proxmox, args),
|
||||
}
|
||||
|
||||
try:
|
||||
if args.command in handlers:
|
||||
return handlers[args.command]()
|
||||
except (RecoveryError, ProxmoxError) as exc:
|
||||
print("Fehler: %s" % exc, file=sys.stderr)
|
||||
return 1
|
||||
except KeyboardInterrupt:
|
||||
print("\nAbgebrochen.", file=sys.stderr)
|
||||
return 130
|
||||
|
||||
# Ohne Befehl: Oberflaeche. Dabei darf nichts ins Terminal protokolliert werden.
|
||||
logging_handlers = logging.getLogger().handlers[:]
|
||||
if not args.verbose:
|
||||
logging.getLogger().handlers = []
|
||||
try:
|
||||
return curses.wrapper(_main, args)
|
||||
except KeyboardInterrupt:
|
||||
return 130
|
||||
finally:
|
||||
logging.getLogger().handlers = logging_handlers
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user