Das eingebaute Austauschlaufwerk faellt weg

Es tat dasselbe wie die Transfer-Laufwerke, nur schlechter: namenlos, nicht
vorher befuellbar, nicht wiederverwendbar, und es starb mit der Maschine. Zwei
Mechanismen fuer dieselbe Sache sind schlechter als einer.

Entfernt: --exchange, --exchange-storage, --exchange-fs, --exchange-dir, die
Unterbefehle pull und release, das Formatieren und Einhaengen auf dem Host
samt Mount-Merkliste. Was davon gebraucht wurde, steckt jetzt in transfer.py -
dort aber benannt, dauerhaft und im laufenden Betrieb wechselbar.

Aeltere Eintraege in der Merkliste stoeren nicht: ihr Austauschlaufwerk ist
ein gewoehnliches Volume der Maschine und wird beim Verwerfen ohnehin von
Proxmox mitgeloescht.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
duffyduck
2026-08-03 11:52:35 +02:00
co-authored by Claude Opus 5
parent 0a92ec0984
commit f2de07ecfd
3 changed files with 82 additions and 525 deletions
+4 -307
View File
@@ -43,8 +43,6 @@ log = logging.getLogger("pvesnap.recovery")
TAG = "pvesnap-recovery"
STATE_DIR = "/var/lib/pvesnap"
REGISTRY = os.path.join(STATE_DIR, "recovery.json")
EXCHANGE_ROOT = os.path.join(STATE_DIR, "exchange")
EXCHANGE_MOUNT = "/run/pvesnap/exchange"
CONF_ROOT = "/etc/pve/nodes"
QEMU_DISK_KEY = re.compile(r"^(ide|sata|scsi|virtio)\d+$")
@@ -65,35 +63,6 @@ DROP_KEYS = {
"replicate", "vmstate", "runningmachine", "runningcpu",
}
DEFAULT_EXCHANGE_SIZE = "10G"
DEFAULT_EXCHANGE_FS = "ext4"
EXCHANGE_LABEL = "PVESNAP"
EXCHANGE_GUEST_PATH = "/mnt/pvesnap"
MKFS = {
"ext4": ["mkfs.ext4", "-q", "-m", "0", "-L", EXCHANGE_LABEL],
"ext3": ["mkfs.ext3", "-q", "-m", "0", "-L", EXCHANGE_LABEL],
"xfs": ["mkfs.xfs", "-q", "-L", EXCHANGE_LABEL],
"vfat": ["mkfs.vfat", "-n", EXCHANGE_LABEL],
"exfat": ["mkfs.exfat", "-L", EXCHANGE_LABEL],
}
README = """Austauschlaufwerk von pvesnap-recovery
Dieses Laufwerk gehoert nicht zur urspruenglichen Maschine. Es ist leer
angelegt worden, damit Daten aus diesem Gast wieder herauskommen - der Gast
hat ja kein Netzwerk.
1. Hier hineinschreiben, z.B.
mysqldump --all-databases > /mnt/pvesnap/dump.sql
2. Gast herunterfahren
3. Auf dem Proxmox-Host: pvesnap-recovery pull <VMID>
Danach liegen die Dateien auf dem Host und lassen sich mit
pvesnap-explorer oder ganz normal per scp abholen.
"""
class RecoveryError(Exception):
"""Eine Wiederherstellung liess sich nicht einrichten."""
@@ -474,10 +443,6 @@ class Spec:
node: str = "" # leer = Node des Originals
net: str = "" # leer = Vorgabe des Modus; none|down|on
resume: object = None # None = RAM-Zustand nehmen, wenn vorhanden
exchange: str = "" # Groesse, z.B. "10G"; leer = keins
exchange_storage: str = ""
exchange_fs: str = DEFAULT_EXCHANGE_FS
exchange_dir: str = "" # nur LXC: vorhandenes Host-Verzeichnis
transfers: list = field(default_factory=list) # Namen vorhandener Laufwerke
memory: int = 0 # 0 = wie im Snapshot
cores: int = 0
@@ -504,8 +469,6 @@ class Plan:
vmstate: str = ""
vmstate_bytes: int = 0
resume: bool = False
exchange_kind: str = "" # "disk" | "bind" | ""
exchange_detail: str = ""
transfers: list = field(default_factory=list) # [transfer.Volume]
warnings: list = field(default_factory=list)
# Was nicht nur unschoen, sondern gefaehrlich ist. Hierfuer genuegt ein
@@ -533,7 +496,6 @@ class Instance:
created: int = 0
volumes: list = field(default_factory=list)
protected: list = field(default_factory=list) # [[volid, snapname]]
exchange: dict = field(default_factory=dict)
transfers: list = field(default_factory=list)
resumed: bool = False
status: str = ""
@@ -559,14 +521,14 @@ class Instance:
def to_dict(self):
return {k: getattr(self, k) for k in
("vmid", "type", "node", "name", "source", "source_node", "snapshot",
"mode", "created", "volumes", "protected", "exchange", "transfers",
"mode", "created", "volumes", "protected", "transfers",
"resumed")}
@classmethod
def from_dict(cls, data):
known = {k: data.get(k) for k in
("vmid", "type", "node", "name", "source", "source_node", "snapshot",
"mode", "created", "volumes", "protected", "exchange", "transfers",
"mode", "created", "volumes", "protected", "transfers",
"resumed") if data.get(k) is not None}
known["vmid"] = int(known.get("vmid") or 0)
return cls(**known)
@@ -770,37 +732,6 @@ def plan(proxmox, guest, snapname, spec):
"gleicher IP und gleicher Identitaet im selben Netz geben Chaos - "
"erst das Original stoppen." % guest.label)
# -- Austauschlaufwerk -------------------------------------------------
if spec.exchange_dir:
if guest.type != "lxc":
raise RecoveryError("--exchange-dir gibt es nur fuer Container. Bei "
"virtuellen Maschinen laesst sich kein Host-"
"Verzeichnis durchreichen - dort --exchange <groesse> "
"verwenden.")
if not os.path.isdir(spec.exchange_dir):
raise RecoveryError("Verzeichnis gibt es nicht: %s" % spec.exchange_dir)
result.exchange_kind = "bind"
result.exchange_detail = "%s -> %s (im Gast)" % (spec.exchange_dir,
EXCHANGE_GUEST_PATH)
elif spec.exchange:
if guest.type == "lxc":
result.exchange_kind = "bind"
result.exchange_detail = "%s/%d -> %s (im Gast)" % (
EXCHANGE_ROOT, result.newid, EXCHANGE_GUEST_PATH)
else:
size_kb = parse_size_kb(spec.exchange)
storage = spec.exchange_storage or result.disks[0][1].split(":", 1)[0]
if spec.exchange_fs not in MKFS:
raise RecoveryError("Dateisystem %r kenne ich nicht (%s)"
% (spec.exchange_fs, ", ".join(sorted(MKFS))))
if not have(MKFS[spec.exchange_fs][0]):
raise RecoveryError("%s ist nicht installiert - anderes Dateisystem "
"waehlen oder Paket nachinstallieren."
% MKFS[spec.exchange_fs][0])
result.exchange_kind = "disk"
result.exchange_detail = "%s auf %s, %s, im Gast als weitere Platte" % (
human_bytes(size_kb * 1024), storage, spec.exchange_fs)
# -- Transfer-Laufwerke ------------------------------------------------
for name in spec.transfers:
volume = transfer.get(name) # wirft, wenn es das nicht gibt
@@ -917,38 +848,6 @@ def create(proxmox, plan_, progress=None):
new_config[key] = value
# -- Austauschlaufwerk ---------------------------------------------
exchange = {}
if plan_.exchange_kind == "disk":
storage = spec.exchange_storage or plan_.disks[0][1].split(":", 1)[0]
size_kb = parse_size_kb(spec.exchange)
step("Lege Austauschlaufwerk an (%s, %s)"
% (human_bytes(size_kb * 1024), spec.exchange_fs))
volid = alloc_volume(storage, plan_.newid, size_kb)
created.append(volid)
_format_exchange(volid, spec.exchange_fs)
slot = _free_disk_slot(new_config)
drive = "%s,backup=0" % volid
exchange = {"kind": "disk", "volid": volid, "key": slot,
"fs": spec.exchange_fs, "drive": drive,
"pending": bool(plan_.resume)}
# Mit geladenem Arbeitsspeicher wacht der Gast in einem Zustand auf,
# in dem es diese Platte nicht gab - er wuerde sie nie bemerken.
# Deshalb kommt sie erst nach dem Fortsetzen dazu, per Hotplug.
if not plan_.resume:
new_config[slot] = drive
elif plan_.exchange_kind == "bind":
host_dir = spec.exchange_dir or os.path.join(EXCHANGE_ROOT,
str(plan_.newid))
if not spec.exchange_dir:
os.makedirs(host_dir, exist_ok=True)
made_dirs.append(host_dir)
_prepare_bind_dir(host_dir, str(config.get("unprivileged")) == "1")
step("Reiche %s in den Container durch" % host_dir)
slot = _free_mp_slot(new_config)
new_config[slot] = "%s,mp=%s" % (host_dir, EXCHANGE_GUEST_PATH)
exchange = {"kind": "bind", "path": host_dir, "key": slot,
"own": not spec.exchange_dir}
# -- Transfer-Laufwerke --------------------------------------------
attached = []
for volume in plan_.transfers:
@@ -996,7 +895,7 @@ def create(proxmox, plan_, progress=None):
name=str(new_config.get("name") or new_config.get("hostname") or ""),
source=guest.vmid, source_node=guest.node, snapshot=plan_.snapshot,
mode=spec.mode, created=int(time.time()), volumes=created,
protected=protected, exchange=exchange, resumed=plan_.resume,
protected=protected, resumed=plan_.resume,
transfers=attached)
_registry_add(instance)
return instance
@@ -1103,29 +1002,6 @@ def _attach_pending(proxmox, instance, step=None):
entry["pending"] = False
_registry_add(instance)
exchange = instance.exchange or {}
if not exchange.get("pending"):
return " ".join(notes)
key, drive = exchange.get("key"), exchange.get("drive")
if not key or not drive:
return ""
if step:
step("Stecke Austauschlaufwerk als %s an" % key)
try:
proxmox.set_guest_config(instance.guest, {key: drive})
config = proxmox.guest_config(instance.guest)
except ProxmoxError as exc:
notes.append("Das Austauschlaufwerk liess sich nicht anstecken: %s" % exc)
return " ".join(notes)
if key not in config:
# Proxmox hat es nur vorgemerkt - im Gast taucht es dann nicht auf.
notes.append("Das Austauschlaufwerk ist eingetragen, aber nicht "
"angesteckt worden (Hotplug fuer Platten ist bei dieser "
"Maschine aus). Es erscheint erst nach einem Neustart.")
else:
exchange["pending"] = False
_registry_add(instance)
return " ".join(notes)
@@ -1312,93 +1188,8 @@ def _copy_state(volid, newid):
# Austauschlaufwerk formatieren
# ---------------------------------------------------------------------------
def _format_exchange(volid, fstype):
with _block_device(volid) as device:
run(MKFS[fstype] + [device], timeout=900)
_place_readme(device, fstype)
class _block_device:
"""Ein Volume als echtes Blockgeraet - noetig, weil `pvesm path` bei Ceph
nur eine qemu-Adresse (rbd:pool/image:...) liefert, kein /dev/..."""
def __init__(self, volid):
self.volid = volid
self.device = ""
self.mapped = False
def __enter__(self):
storage_op("activate", self.volid, timeout=300)
path = volume_path(self.volid)
if path.startswith("rbd:"):
self.device = self._map_rbd(path)
self.mapped = True
else:
self.device = path
if not os.path.exists(self.device):
raise RecoveryError("%s ist nicht als Geraet aufgetaucht" % self.device)
return self.device
def __exit__(self, *_exc):
if self.mapped and self.device:
run(["rbd", "unmap", self.device], check=False, timeout=120)
return False
@staticmethod
def _map_rbd(path):
# rbd:data/vm-9802-disk-1:conf=/etc/pve/ceph.conf:id=admin:keyring=...
fields = path[4:].split(":")
image = fields[0]
args = ["rbd", "map", image]
for field_ in fields[1:]:
key, _, value = field_.partition("=")
if key == "id":
args += ["--id", value]
elif key == "keyring":
args += ["--keyring", value]
elif key == "conf":
args += ["--conf", value]
elif key == "mon_host":
args += ["-m", value.replace(";", ",")]
output = run(args, timeout=180)
for line in output.splitlines():
if line.strip().startswith("/dev/"):
return line.strip()
raise RecoveryError("rbd map lieferte keinen Geraetenamen: %s" % output)
def _place_readme(device, fstype):
"""Kurze Anleitung auf das leere Laufwerk legen."""
mountpoint = os.path.join(EXCHANGE_MOUNT, "format-%d" % os.getpid())
try:
os.makedirs(mountpoint, exist_ok=True)
run(["mount", "-t", fstype, device, mountpoint], timeout=120)
except (RecoveryError, OSError) as exc:
log.debug("Anleitung nicht ablegbar: %s", exc)
return
try:
# Ohne das gehoert das Wurzelverzeichnis root, und ein gewoehnlicher
# Benutzer im Gast kann nichts hineinschreiben - was sich anfuehlt wie
# ein schreibgeschuetzter Datentraeger, aber keiner ist.
try:
os.chmod(mountpoint, 0o777)
except OSError:
pass
with open(os.path.join(mountpoint, "LIESMICH.txt"), "w",
encoding="utf-8") as handle:
handle.write(README)
except OSError as exc:
log.debug("LIESMICH.txt nicht schreibbar: %s", exc)
finally:
run(["umount", mountpoint], check=False, timeout=120)
try:
os.rmdir(mountpoint)
except OSError:
pass
# ---------------------------------------------------------------------------
# Uebersicht, Verwerfen, Austausch abholen
# Uebersicht und Verwerfen
# ---------------------------------------------------------------------------
def list_instances(proxmox, refresh=True):
@@ -1632,99 +1423,5 @@ def destroy(proxmox, instance, progress=None, keep_snapshot_protection=False):
except transfer.TransferError as exc:
log.warning("%s: %s", name, exc)
exchange = instance.exchange or {}
if exchange.get("kind") == "bind" and exchange.get("own") and exchange.get("path"):
step("Entferne Austauschverzeichnis %s" % exchange["path"])
shutil.rmtree(exchange["path"], ignore_errors=True)
_registry_remove(instance.vmid)
step("Fertig.")
def pull(proxmox, instance):
"""Austauschlaufwerk auf dem Host verfuegbar machen; liefert den Pfad."""
exchange = instance.exchange or {}
if not exchange:
raise RecoveryError("%s hat kein Austauschlaufwerk." % instance.label)
if exchange.get("kind") == "bind":
path = exchange.get("path") or ""
if not os.path.isdir(path):
raise RecoveryError("Austauschverzeichnis gibt es nicht: %s" % path)
return path
volid = exchange.get("volid")
if not volid:
raise RecoveryError("Zu %s ist kein Austauschlaufwerk vermerkt."
% instance.label)
status = proxmox.guest_status(instance.guest)
if status.get("status") == "running":
raise RecoveryError("%s laeuft noch. Erst herunterfahren - sonst schreiben "
"Gast und Host gleichzeitig auf dasselbe Dateisystem."
% instance.label)
mountpoint = os.path.join(EXCHANGE_MOUNT, str(instance.vmid))
if os.path.ismount(mountpoint):
return mountpoint
os.makedirs(mountpoint, exist_ok=True)
holder = _block_device(volid)
device = holder.__enter__()
try:
fstype = exchange.get("fs") or "auto"
run(["mount", "-o", "ro", "-t", fstype, device, mountpoint], timeout=120)
except RecoveryError:
holder.__exit__()
raise
_remember_mount(instance.vmid, mountpoint, device, holder.mapped)
return mountpoint
def release(vmid):
"""Ein mit pull() eingehaengtes Austauschlaufwerk wieder loesen."""
entry = _mounts().pop(str(vmid), None)
_save_mounts(_mounts_without(vmid))
if not entry:
return False
run(["umount", entry["mountpoint"]], check=False, timeout=120)
if entry.get("mapped"):
run(["rbd", "unmap", entry["device"]], check=False, timeout=120)
try:
os.rmdir(entry["mountpoint"])
except OSError:
pass
return True
_MOUNTS = os.path.join("/run/pvesnap", "recovery-mounts.json")
def _mounts():
try:
with open(_MOUNTS, "r", encoding="utf-8") as handle:
data = json.load(handle)
return data if isinstance(data, dict) else {}
except (OSError, ValueError):
return {}
def _mounts_without(vmid):
data = _mounts()
data.pop(str(vmid), None)
return data
def _save_mounts(data):
try:
os.makedirs(os.path.dirname(_MOUNTS), exist_ok=True)
with open(_MOUNTS, "w", encoding="utf-8") as handle:
json.dump(data, handle, indent=1)
except OSError as exc:
log.debug("Mount-Merkliste nicht schreibbar: %s", exc)
def _remember_mount(vmid, mountpoint, device, mapped):
data = _mounts()
data[str(vmid)] = {"mountpoint": mountpoint, "device": device, "mapped": mapped}
_save_mounts(data)
+20 -175
View File
@@ -5,10 +5,10 @@ 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 live 802 <snapshot> --transfer dumps
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 Taste v: Transfer-Laufwerke
pvesnap-recovery destroy 9802 restlos verwerfen
"""
@@ -26,11 +26,10 @@ 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, RecoveryError, Spec, create,
from .recovery import (RecoveryError, Spec, create,
eject_transfer, insert_transfer, transfer_state,
destroy, find_orphans, flatten, flatten_cost, flatten_hint,
human_bytes,
linked_volumes, list_instances, plan, pull, release,
human_bytes, linked_volumes, list_instances, plan,
remove_orphans, start)
from . import transfer, transfer_ui
from .snapfs import list_snapshots
@@ -43,7 +42,6 @@ KEYS = [
("n", "Neu", "Neu"),
("s", "Starten", "Start"),
("h", "Herunterfahren", "Stop"),
("a", "Austausch", "Austau."),
("x", "Verwerfen", "Verwerf."),
("v", "Laufwerke", "Laufw."),
("r", "Aktualisieren", "Aktual."),
@@ -186,12 +184,8 @@ def _field_text(spec, key, kind, choices):
return str(value) if value else "automatisch (naechste freie)"
if key == "transfers":
return transfer_ui.summary(value)
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 "")
@@ -280,10 +274,8 @@ def _edit(win, spec, key, kind, choices, back=False):
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)
label = {"newid": "Neue VMID (leer = automatisch)",
"node": "Node (leer = wie das Original)"}.get(key, key)
answer = prompt(win, label, str(getattr(spec, key) or ""))
if answer is None:
return
@@ -329,8 +321,6 @@ def summary_lines(plan_):
% (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))
for volume in plan_.transfers:
lines.append(("Transfer: %-14s %8s im Gast als %s"
% (volume.name, human_bytes(volume.size), volume.label),
@@ -380,7 +370,7 @@ def _new(stdscr, proxmox):
if snapname is None:
continue
spec = Spec(exchange_dir="")
spec = Spec()
while True:
if not _options(stdscr, guest, spec):
break
@@ -460,17 +450,6 @@ def _details(stdscr, proxmox, instance):
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 "
@@ -512,7 +491,7 @@ def _details(stdscr, proxmox, instance):
lines.append(("Datentraeger: eigenstaendig - haengt an keinem Snapshot",
curses.color_pair(C_OK)))
footer = ("s Start | h Stop | e Auswerfen | a Austausch | f Loesen | "
footer = ("s Start | h Stop | e Auswerfen | f Loesen | "
"x Verwerfen | q zurueck")
key = _pages(stdscr, lines, "Wiederherstellung %s" % instance.label, footer)
@@ -524,8 +503,6 @@ def _details(stdscr, 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 ("e", "E"):
_swap(stdscr, proxmox, instance, steckplaetze)
elif key in ("f", "F"):
@@ -546,55 +523,6 @@ def _do(stdscr, text, action):
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 _swap(stdscr, proxmox, instance, steckplaetze):
"""Transfer-Laufwerk im laufenden Betrieb abziehen oder wieder einklinken."""
wechselbar = [(e, drin) for e, drin in steckplaetze if e.get("kind") != "bind"]
if not wechselbar:
message(stdscr, "Hier gibt es nichts zu wechseln.", error=True)
return
if len(wechselbar) == 1:
entry, drin = wechselbar[0]
else:
gewaehlt = choose(stdscr, "Welches Laufwerk?",
[((e, d), "%-14s %s" % (e["name"],
"eingesteckt" if d else "ausgeworfen"))
for e, d in wechselbar])
if gewaehlt is None:
return
entry, drin = gewaehlt
name = entry["name"]
if drin:
if not confirm(stdscr, "%r auswerfen? Im Gast vorher aushaengen!" % name):
return
_do(stdscr, "Werfe %r aus ..." % name,
lambda: eject_transfer(proxmox, instance, name))
else:
_do(stdscr, "Klinke %r ein ..." % name,
lambda: insert_transfer(proxmox, instance, name))
def _flatten(stdscr, proxmox, instance, linked):
if not linked:
message(stdscr, "%s haengt an keinem Snapshot mehr." % instance.label)
@@ -721,8 +649,6 @@ def _overview(stdscr, proxmox):
_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
@@ -786,18 +712,12 @@ def _spec_from_args(args, mode):
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 = "exfat"
spec.transfers = list(args.transfer or [])
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
@@ -814,8 +734,9 @@ def _print_summary(plan_):
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 volume in plan_.transfers:
print(" Transfer : %-14s %8s im Gast als %s"
% (volume.name, human_bytes(volume.size), volume.label))
for note in plan_.notes:
print(" Hinweis : %s" % note)
for warning in plan_.warnings:
@@ -887,15 +808,6 @@ def _print_access(instance):
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)
@@ -925,50 +837,6 @@ def cmd_destroy(proxmox, args):
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_flatten(proxmox, args):
instance = _find_instance(proxmox, args.vmid)
linked = linked_volumes(proxmox, instance)
if not linked:
print("%s haengt an keinem Snapshot mehr - nichts zu tun."
% instance.label)
return 0
print("%s loest sich von %s." % (instance.label, instance.origin))
for volid in linked:
print(" %s" % volid)
cost = flatten_cost(proxmox, instance)
print("\nDabei werden die Daten wirklich kopiert%s."
% ((" - rund %s" % human_bytes(cost)) if cost else ""))
if not args.yes:
if _ask("Jetzt loesen? [j/N] ") not in ("j", "y", "ja", "yes"):
print("Abgebrochen.")
return 1
done, already, other = flatten(proxmox, instance, stream=True, force=args.force,
progress=lambda text: print(" " + text))
print("\n%d Datentraeger geloest." % len(done))
for volid in already:
print(" war schon eigenstaendig: %s" % volid)
for volid in other:
print(" %s: %s" % (volid, flatten_hint(proxmox, volid)))
if not linked_volumes(proxmox, instance):
print("\n%s ist jetzt eigenstaendig. Die Snapshots des Originals lassen "
"sich wieder loeschen." % instance.label)
return 0
def cmd_cleanup(proxmox, args):
orphans = find_orphans(proxmox)
if not orphans:
@@ -1009,11 +877,6 @@ def cmd_cleanup(proxmox, args):
return 1 if failed else 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())
@@ -1048,17 +911,9 @@ def _add_create_options(parser):
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("--transfer", action="append", metavar="NAME", default=[],
help="vorbereitetes Transfer-Laufwerk anhaengen "
"(mehrfach moeglich)")
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")
@@ -1083,16 +938,16 @@ def build_parser():
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 802 --transfer dumps
neuesten Snapshot von VM 802 ohne Netzwerk starten, mit dem
vorbereiteten Transfer-Laufwerk "dumps"
pvesnap-recovery live 100 --exchange ja
Container 100 abgeschottet starten; ein Host-Verzeichnis erscheint
im Container unter %s
pvesnap-recovery live 100 --transfer dumps --transfer werkzeuge
Container 100 abgeschottet starten, mit zwei Laufwerken
pvesnap-recovery live 100 --exchange-dir /mnt/nfs/dumps
dasselbe, aber mit einem eigenen (auch eingehaengten) Verzeichnis
pvesnap-recovery
Taste v: Transfer-Laufwerke anlegen und befuellen (Commander)
Taste e: im laufenden Betrieb auswerfen und wieder einklinken
pvesnap-recovery recover 802 autosnap-... --newid 9802
mit Netzwerk und gleicher Identitaet - nur wenn das Original steht!
@@ -1102,7 +957,7 @@ def build_parser():
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__)
@@ -1123,14 +978,6 @@ def build_parser():
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)
flatten_ = subs.add_parser(
"flatten", help="vom Quell-Snapshot loesen - fuer den Dauerbetrieb")
flatten_.add_argument("vmid", type=int)
@@ -1170,8 +1017,6 @@ def main(argv=None):
"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),
"flatten": lambda: cmd_flatten(proxmox, args),
"cleanup": lambda: cmd_cleanup(proxmox, args),
"console": lambda: cmd_console(proxmox, args),