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
+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),