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
+58 -43
View File
@@ -770,7 +770,7 @@ passen. `pvesnap-recovery` sorgt selbst dafür:
* `runningmachine` und `runningcpu` aus dem Snapshot werden übernommen * `runningmachine` und `runningcpu` aus dem Snapshot werden übernommen
* die Netzwerkkarte bleibt **vorhanden**, wird aber abgeklemmt (`link_down=1`) * die Netzwerkkarte bleibt **vorhanden**, wird aber abgeklemmt (`link_down=1`)
statt entfernt — herausnehmen würde den Zustand unbrauchbar machen statt entfernt — herausnehmen würde den Zustand unbrauchbar machen
* das Austauschlaufwerk wird erst **nach** dem Fortsetzen angesteckt (Hotplug), * Transfer-Laufwerke werden erst **nach** dem Fortsetzen angesteckt (Hotplug),
weil der Gast in einem Zustand aufwacht, in dem es die Platte noch nicht gab weil der Gast in einem Zustand aufwacht, in dem es die Platte noch nicht gab
Ein Detail, das Proxmox nicht selbst meldet: Schlägt das Laden des Ein Detail, das Proxmox nicht selbst meldet: Schlägt das Laden des
@@ -781,49 +781,67 @@ gebootet wurde.
Mit `--no-resume` lässt sich der Arbeitsspeicher bewusst weglassen. Mit `--no-resume` lässt sich der Arbeitsspeicher bewusst weglassen.
### Daten aus einer Maschine ohne Netzwerk holen ### Daten hinein und wieder heraus — Transfer-Laufwerke
Ohne Netz kommt nichts heraus — dafür gibt es das **Austauschlaufwerk**. Ein wiederhergestellter Snapshot hat kein Netzwerk. Dafür gibt es
**Transfer-Laufwerke**: benannte Abbilddateien unter
`/var/lib/pvesnap/transfer/`, die du **in Ruhe vorher** anlegst und befüllst
und im Notfall nur noch aus einer Liste auswählst.
**Bei virtuellen Maschinen** wird eine leere, formatierte Zusatzplatte angelegt ```
(Bezeichnung `PVESNAP`, mit einer kurzen `LIESMICH.txt` darauf): pvesnap-recovery
v Übersicht der Laufwerke
```bash n "dumps", 20G ← einmalig
# Snapshot abgeschottet starten, mit 20 GB Austauschplatte Enter Commander: Werkzeuge hineinkopieren
pvesnap-recovery live 802 --exchange 20G q q
n Wiederherstellung, Snapshot wählen
# im Gast (über die noVNC-Konsole): Transfer-Laufwerke: [dumps] ← Leertaste
mount /dev/disk/by-label/PVESNAP /mnt F10 läuft
mysqldump --all-databases > /mnt/dump.sql ...arbeiten... im Gast: Datenträger DUMPS
umount /mnt && poweroff e auswerfen, am Host auslesen, wieder einklinken
h herunterfahren
# auf dem Host: v Enter Commander: Ergebnis abholen
pvesnap-recovery pull 9802 # gibt den Pfad aus, z. B. /run/pvesnap/exchange/9802
pvesnap-recovery release 9802 # wieder lösen
pvesnap-recovery destroy 9802 # alles verwerfen
``` ```
**Bei Containern** ist es einfacher: Ein Host-Verzeichnis wird direkt Kein einziger Parameter. Auf der Kommandozeile ginge es auch:
durchgereicht und erscheint im Container unter `/mnt/pvesnap`. Was der Container `--transfer dumps`, mehrfach möglich.
hineinschreibt, liegt sofort auf dem Host — kein Ein- und Aushängen nötig.
```bash | Eigenschaft | |
pvesnap-recovery live 100 --exchange ja |---|---|
pct exec 9100 -- sh -c 'pg_dumpall > /mnt/pvesnap/dump.sql' | Dateisystem | **exFAT** — Windows und Linux nativ, keine 4-GB-Grenze je Datei |
ls /var/lib/pvesnap/exchange/9100/ | Partition | GPT mit `msftdata`, damit Windows sofort einen Laufwerksbuchstaben vergibt |
| Bezeichnung | der Name in Großbuchstaben, z. B. `DUMPS` (max. 11 Zeichen) |
| Lebensdauer | überlebt jede Wiederherstellung — auch das Löschen der VM in der Weboberfläche |
**Warum eine Datei und kein Proxmox-Volume?** Ein Volume würde beim Entfernen
der Maschine mitgelöscht, auch aus der Weboberfläche heraus — die mühsam
vorbereitete Werkzeugsammlung wäre weg. Pfade überspringt PVE beim Zerstören
ausdrücklich (`return if $volid =~ m|^/|`). Angehängt wird über ein
Loop-Gerät, weil Proxmox als Pfad nur `/dev/…` akzeptiert.
**Host oder Gast, nie beides.** Zwei unabhängige Einhängungen desselben
Blockgeräts zerlegen das Dateisystem. Die Verwaltung lässt das nicht zu:
Anhängen wird abgelehnt, solange der Host es hält; Aushängen, solange ein Gast
darauf arbeitet; Löschen, solange es überhaupt in Benutzung ist.
**Bei Containern** ist es kein Blockgerät, sondern ein durchgereichtes
Verzeichnis. Host und Container sehen dieselben Dateien **gleichzeitig**
dort gibt es nichts auszuwerfen und nichts zu warten.
### Im laufenden Betrieb wechseln
Taste `e` in der Detailansicht zieht das Laufwerk bei laufender Maschine ab
und gibt es wieder hinein — beliebig oft:
```
auswerfen → gehört wieder dem Host, einhängen und befüllen
einklinken → zurück in denselben Steckplatz, VM läuft durchgehend
``` ```
Und hier passt auch die Idee mit dem **Netzlaufwerk**: Ist auf dem Host ohnehin > **Im Gast vorher aushängen.** Unter Linux `umount`, unter Windows
schon eine NFS- oder CIFS-Freigabe eingehängt, lässt sie sich direkt > „Auswerfen" im Explorer. Proxmox meldet das Gerät zwar ordentlich ab, aber
durchreichen — der Container schreibt dann quasi aufs Netzlaufwerk, obwohl er > ein Dateisystem, auf das gerade geschrieben wird, nimmt das übel. Hält der
selbst kein Netzwerk hat: > Gast es fest, schlägt das Auswerfen mit einer entsprechenden Meldung fehl.
```bash
pvesnap-recovery live 100 --exchange-dir /mnt/nfs/dumps
```
Bei virtuellen Maschinen geht das nicht: Ein Host-Verzeichnis lässt sich nicht in
eine VM hineinreichen. Dort bleibt es bei der Austauschplatte.
### An die Konsole kommen ### An die Konsole kommen
@@ -871,10 +889,7 @@ Karte erst zuschalten, wenn klar ist, dass die Luft rein ist.
| `--node <name>` | auf welchem Node sie laufen soll | | `--node <name>` | auf welchem Node sie laufen soll |
| `--net none\|down\|on` | keine Karte / Karte ohne Leitung / voll am Netz | | `--net none\|down\|on` | keine Karte / Karte ohne Leitung / voll am Netz |
| `--resume` / `--no-resume` | Arbeitsspeicher laden bzw. bewusst kalt starten | | `--resume` / `--no-resume` | Arbeitsspeicher laden bzw. bewusst kalt starten |
| `--exchange <größe>` | Austauschplatte, z. B. `20G` (Container: `ja` genügt) | | `--transfer <name>` | vorbereitetes Transfer-Laufwerk anhängen (mehrfach möglich) |
| `--exchange-storage <name>` | Storage dafür (Vorgabe: wie die erste Platte) |
| `--exchange-fs <fs>` | `ext4` (Vorgabe), `ext3`, `xfs`, `vfat`, `exfat` |
| `--exchange-dir <pfad>` | nur Container: dieses Host-Verzeichnis durchreichen |
| `--memory <MB>`, `--cores <n>` | abweichende Ausstattung (nicht mit `--resume`) | | `--memory <MB>`, `--cores <n>` | abweichende Ausstattung (nicht mit `--resume`) |
| `--name <name>` | Name der neuen Maschine | | `--name <name>` | Name der neuen Maschine |
| `--keep-binds` | durchgereichte Host-Verzeichnisse des Originals übernehmen | | `--keep-binds` | durchgereichte Host-Verzeichnisse des Originals übernehmen |
@@ -888,8 +903,6 @@ Karte erst zuschalten, wenn klar ist, dass die Luft rein ist.
| `list` | vorhandene Wiederherstellungen anzeigen | | `list` | vorhandene Wiederherstellungen anzeigen |
| `start` / `stop` | hoch- bzw. herunterfahren | | `start` / `stop` | hoch- bzw. herunterfahren |
| `console <VMID>` | noVNC-Adresse ausgeben | | `console <VMID>` | noVNC-Adresse ausgeben |
| `pull <VMID>` | Austauschlaufwerk auf den Host holen |
| `release <VMID>` | es wieder lösen |
| `flatten <VMID>` | vom Quell-Snapshot lösen (für den Dauerbetrieb) | | `flatten <VMID>` | vom Quell-Snapshot lösen (für den Dauerbetrieb) |
| `cleanup` | Klone entfernen, zu denen es keinen Gast mehr gibt | | `cleanup` | Klone entfernen, zu denen es keinen Gast mehr gibt |
| `destroy <VMID>` | restlos verwerfen | | `destroy <VMID>` | restlos verwerfen |
@@ -932,6 +945,8 @@ pvesnap/
explorer.py Zwei-Fenster-Explorer im Terminal explorer.py Zwei-Fenster-Explorer im Terminal
recovery.py Snapshot als Maschine starten (Klone, Konfiguration, Aufräumen) recovery.py Snapshot als Maschine starten (Klone, Konfiguration, Aufräumen)
recovery_ui.py ncurses-Oberfläche und Kommandozeile dazu recovery_ui.py ncurses-Oberfläche und Kommandozeile dazu
transfer.py Transfer-Laufwerke: anlegen, ein-/aushängen, Verriegelung
transfer_ui.py deren Bildschirm, auch aus dem Explorer erreichbar
web/ Web-Oberfläche zum Herunterladen web/ Web-Oberfläche zum Herunterladen
server.py HTTP-Server, ZIP im Strom, Zugangsschlüssel server.py HTTP-Server, ZIP im Strom, Zugangsschlüssel
assets.py Stylesheet und HTML-Gerüst assets.py Stylesheet und HTML-Gerüst
+4 -307
View File
@@ -43,8 +43,6 @@ log = logging.getLogger("pvesnap.recovery")
TAG = "pvesnap-recovery" TAG = "pvesnap-recovery"
STATE_DIR = "/var/lib/pvesnap" STATE_DIR = "/var/lib/pvesnap"
REGISTRY = os.path.join(STATE_DIR, "recovery.json") 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" CONF_ROOT = "/etc/pve/nodes"
QEMU_DISK_KEY = re.compile(r"^(ide|sata|scsi|virtio)\d+$") QEMU_DISK_KEY = re.compile(r"^(ide|sata|scsi|virtio)\d+$")
@@ -65,35 +63,6 @@ DROP_KEYS = {
"replicate", "vmstate", "runningmachine", "runningcpu", "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): class RecoveryError(Exception):
"""Eine Wiederherstellung liess sich nicht einrichten.""" """Eine Wiederherstellung liess sich nicht einrichten."""
@@ -474,10 +443,6 @@ class Spec:
node: str = "" # leer = Node des Originals node: str = "" # leer = Node des Originals
net: str = "" # leer = Vorgabe des Modus; none|down|on net: str = "" # leer = Vorgabe des Modus; none|down|on
resume: object = None # None = RAM-Zustand nehmen, wenn vorhanden 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 transfers: list = field(default_factory=list) # Namen vorhandener Laufwerke
memory: int = 0 # 0 = wie im Snapshot memory: int = 0 # 0 = wie im Snapshot
cores: int = 0 cores: int = 0
@@ -504,8 +469,6 @@ class Plan:
vmstate: str = "" vmstate: str = ""
vmstate_bytes: int = 0 vmstate_bytes: int = 0
resume: bool = False resume: bool = False
exchange_kind: str = "" # "disk" | "bind" | ""
exchange_detail: str = ""
transfers: list = field(default_factory=list) # [transfer.Volume] transfers: list = field(default_factory=list) # [transfer.Volume]
warnings: list = field(default_factory=list) warnings: list = field(default_factory=list)
# Was nicht nur unschoen, sondern gefaehrlich ist. Hierfuer genuegt ein # Was nicht nur unschoen, sondern gefaehrlich ist. Hierfuer genuegt ein
@@ -533,7 +496,6 @@ class Instance:
created: int = 0 created: int = 0
volumes: list = field(default_factory=list) volumes: list = field(default_factory=list)
protected: list = field(default_factory=list) # [[volid, snapname]] protected: list = field(default_factory=list) # [[volid, snapname]]
exchange: dict = field(default_factory=dict)
transfers: list = field(default_factory=list) transfers: list = field(default_factory=list)
resumed: bool = False resumed: bool = False
status: str = "" status: str = ""
@@ -559,14 +521,14 @@ class Instance:
def to_dict(self): def to_dict(self):
return {k: getattr(self, k) for k in return {k: getattr(self, k) for k in
("vmid", "type", "node", "name", "source", "source_node", "snapshot", ("vmid", "type", "node", "name", "source", "source_node", "snapshot",
"mode", "created", "volumes", "protected", "exchange", "transfers", "mode", "created", "volumes", "protected", "transfers",
"resumed")} "resumed")}
@classmethod @classmethod
def from_dict(cls, data): def from_dict(cls, data):
known = {k: data.get(k) for k in known = {k: data.get(k) for k in
("vmid", "type", "node", "name", "source", "source_node", "snapshot", ("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} "resumed") if data.get(k) is not None}
known["vmid"] = int(known.get("vmid") or 0) known["vmid"] = int(known.get("vmid") or 0)
return cls(**known) return cls(**known)
@@ -770,37 +732,6 @@ def plan(proxmox, guest, snapname, spec):
"gleicher IP und gleicher Identitaet im selben Netz geben Chaos - " "gleicher IP und gleicher Identitaet im selben Netz geben Chaos - "
"erst das Original stoppen." % guest.label) "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 ------------------------------------------------ # -- Transfer-Laufwerke ------------------------------------------------
for name in spec.transfers: for name in spec.transfers:
volume = transfer.get(name) # wirft, wenn es das nicht gibt volume = transfer.get(name) # wirft, wenn es das nicht gibt
@@ -917,38 +848,6 @@ def create(proxmox, plan_, progress=None):
new_config[key] = value new_config[key] = value
# -- Austauschlaufwerk --------------------------------------------- # -- 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 -------------------------------------------- # -- Transfer-Laufwerke --------------------------------------------
attached = [] attached = []
for volume in plan_.transfers: 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 ""), name=str(new_config.get("name") or new_config.get("hostname") or ""),
source=guest.vmid, source_node=guest.node, snapshot=plan_.snapshot, source=guest.vmid, source_node=guest.node, snapshot=plan_.snapshot,
mode=spec.mode, created=int(time.time()), volumes=created, mode=spec.mode, created=int(time.time()), volumes=created,
protected=protected, exchange=exchange, resumed=plan_.resume, protected=protected, resumed=plan_.resume,
transfers=attached) transfers=attached)
_registry_add(instance) _registry_add(instance)
return instance return instance
@@ -1103,29 +1002,6 @@ def _attach_pending(proxmox, instance, step=None):
entry["pending"] = False entry["pending"] = False
_registry_add(instance) _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) return " ".join(notes)
@@ -1312,93 +1188,8 @@ def _copy_state(volid, newid):
# Austauschlaufwerk formatieren # 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): 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: except transfer.TransferError as exc:
log.warning("%s: %s", name, 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) _registry_remove(instance.vmid)
step("Fertig.") 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 Uebersicht und Assistent
pvesnap-recovery list was gerade laeuft pvesnap-recovery list was gerade laeuft
pvesnap-recovery live 802 neuesten Snapshot ohne Netz starten 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 recover 802 <snapshot> mit Netz, gleiche Identitaet
pvesnap-recovery console 9802 noVNC-Adresse anzeigen 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 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, choose, confirm, fill, init_colors, is_enter, is_escape,
keybar, message, prompt, put, read_key) keybar, message, prompt, put, read_key)
from .proxmox import Proxmox, ProxmoxError, pvesh_available 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, eject_transfer, insert_transfer, transfer_state,
destroy, find_orphans, flatten, flatten_cost, flatten_hint, destroy, find_orphans, flatten, flatten_cost, flatten_hint,
human_bytes, human_bytes, linked_volumes, list_instances, plan,
linked_volumes, list_instances, plan, pull, release,
remove_orphans, start) remove_orphans, start)
from . import transfer, transfer_ui from . import transfer, transfer_ui
from .snapfs import list_snapshots from .snapfs import list_snapshots
@@ -43,7 +42,6 @@ KEYS = [
("n", "Neu", "Neu"), ("n", "Neu", "Neu"),
("s", "Starten", "Start"), ("s", "Starten", "Start"),
("h", "Herunterfahren", "Stop"), ("h", "Herunterfahren", "Stop"),
("a", "Austausch", "Austau."),
("x", "Verwerfen", "Verwerf."), ("x", "Verwerfen", "Verwerf."),
("v", "Laufwerke", "Laufw."), ("v", "Laufwerke", "Laufw."),
("r", "Aktualisieren", "Aktual."), ("r", "Aktualisieren", "Aktual."),
@@ -186,12 +184,8 @@ def _field_text(spec, key, kind, choices):
return str(value) if value else "automatisch (naechste freie)" return str(value) if value else "automatisch (naechste freie)"
if key == "transfers": if key == "transfers":
return transfer_ui.summary(value) return transfer_ui.summary(value)
if key == "exchange":
return value or "keins"
if key == "node": if key == "node":
return value or "wie das Original" return value or "wie das Original"
if key == "exchange_dir":
return value or "(eigenes anlegen)"
return str(value or "") return str(value or "")
@@ -280,10 +274,8 @@ def _edit(win, spec, key, kind, choices, back=False):
spec.net = "" spec.net = ""
return return
label = {"exchange": "Groesse (z.B. 20G, leer = keins)", label = {"newid": "Neue VMID (leer = automatisch)",
"newid": "Neue VMID (leer = automatisch)", "node": "Node (leer = wie das Original)"}.get(key, key)
"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 "")) answer = prompt(win, label, str(getattr(spec, key) or ""))
if answer is None: if answer is None:
return return
@@ -329,8 +321,6 @@ def summary_lines(plan_):
% (human_bytes(plan_.vmstate_bytes) or "?") % (human_bytes(plan_.vmstate_bytes) or "?")
if plan_.resume else "vorhanden, wird aber nicht geladen"), if plan_.resume else "vorhanden, wird aber nicht geladen"),
curses.color_pair(C_OK) if plan_.resume else 0)) 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: for volume in plan_.transfers:
lines.append(("Transfer: %-14s %8s im Gast als %s" lines.append(("Transfer: %-14s %8s im Gast als %s"
% (volume.name, human_bytes(volume.size), volume.label), % (volume.name, human_bytes(volume.size), volume.label),
@@ -380,7 +370,7 @@ def _new(stdscr, proxmox):
if snapname is None: if snapname is None:
continue continue
spec = Spec(exchange_dir="") spec = Spec()
while True: while True:
if not _options(stdscr, guest, spec): if not _options(stdscr, guest, spec):
break break
@@ -460,17 +450,6 @@ def _details(stdscr, proxmox, instance):
lines.append((" oder auf dem Host: pct enter %d" % instance.vmid, lines.append((" oder auf dem Host: pct enter %d" % instance.vmid,
curses.color_pair(C_MARK))) 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: if instance.resumed:
lines.append(("", 0)) lines.append(("", 0))
lines.append(("Der Arbeitsspeicher des Snapshots wurde geladen - beim " 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", lines.append(("Datentraeger: eigenstaendig - haengt an keinem Snapshot",
curses.color_pair(C_OK))) 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") "x Verwerfen | q zurueck")
key = _pages(stdscr, lines, "Wiederherstellung %s" % instance.label, footer) key = _pages(stdscr, lines, "Wiederherstellung %s" % instance.label, footer)
@@ -524,8 +503,6 @@ def _details(stdscr, proxmox, instance):
elif key in ("h", "H"): elif key in ("h", "H"):
_do(stdscr, "Fahre %s herunter ..." % instance.label, _do(stdscr, "Fahre %s herunter ..." % instance.label,
lambda: proxmox.shutdown_guest(instance.guest)) lambda: proxmox.shutdown_guest(instance.guest))
elif key in ("a", "A"):
_pull(stdscr, proxmox, instance)
elif key in ("e", "E"): elif key in ("e", "E"):
_swap(stdscr, proxmox, instance, steckplaetze) _swap(stdscr, proxmox, instance, steckplaetze)
elif key in ("f", "F"): elif key in ("f", "F"):
@@ -546,55 +523,6 @@ def _do(stdscr, text, action):
message(stdscr, hint, error=True) 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): def _flatten(stdscr, proxmox, instance, linked):
if not linked: if not linked:
message(stdscr, "%s haengt an keinem Snapshot mehr." % instance.label) 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, _do(stdscr, "Fahre %s herunter ..." % current.label,
lambda: proxmox.shutdown_guest(current.guest)) lambda: proxmox.shutdown_guest(current.guest))
reload_ = True reload_ = True
elif key in ("a", "A"):
_pull(stdscr, proxmox, current)
elif key in ("x", "X"): elif key in ("x", "X"):
_destroy(stdscr, proxmox, current) _destroy(stdscr, proxmox, current)
reload_ = True reload_ = True
@@ -786,18 +712,12 @@ def _spec_from_args(args, mode):
spec.node = args.node or "" spec.node = args.node or ""
spec.net = args.net or "" spec.net = args.net or ""
spec.resume = args.resume 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.transfers = list(args.transfer or [])
spec.exchange_dir = args.exchange_dir or ""
spec.memory = args.memory or 0 spec.memory = args.memory or 0
spec.cores = args.cores or 0 spec.cores = args.cores or 0
spec.name = args.name or "" spec.name = args.name or ""
spec.keep_binds = args.keep_binds spec.keep_binds = args.keep_binds
spec.start = not args.no_start spec.start = not args.no_start
if spec.exchange_dir and not spec.exchange:
spec.exchange = "bind"
return spec return spec
@@ -814,8 +734,9 @@ def _print_summary(plan_):
if plan_.resume: if plan_.resume:
print(" Arbeitsspei.: wird geladen (%s)" print(" Arbeitsspei.: wird geladen (%s)"
% (human_bytes(plan_.vmstate_bytes) or "?")) % (human_bytes(plan_.vmstate_bytes) or "?"))
if plan_.exchange_kind: for volume in plan_.transfers:
print(" Austausch : %s" % plan_.exchange_detail) print(" Transfer : %-14s %8s im Gast als %s"
% (volume.name, human_bytes(volume.size), volume.label))
for note in plan_.notes: for note in plan_.notes:
print(" Hinweis : %s" % note) print(" Hinweis : %s" % note)
for warning in plan_.warnings: for warning in plan_.warnings:
@@ -887,15 +808,6 @@ def _print_access(instance):
print(" %s" % instance.console_url()) print(" %s" % instance.console_url())
if instance.type == "lxc": if instance.type == "lxc":
print(" oder auf dem Host: pct enter %d" % instance.vmid) 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) print("\n Wieder verwerfen: pvesnap-recovery destroy %d" % instance.vmid)
@@ -925,50 +837,6 @@ def cmd_destroy(proxmox, args):
return 0 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): def cmd_cleanup(proxmox, args):
orphans = find_orphans(proxmox) orphans = find_orphans(proxmox)
if not orphans: if not orphans:
@@ -1009,11 +877,6 @@ def cmd_cleanup(proxmox, args):
return 1 if failed else 0 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): def cmd_console(proxmox, args):
instance = _find_instance(proxmox, args.vmid) instance = _find_instance(proxmox, args.vmid)
print(instance.console_url()) print(instance.console_url())
@@ -1048,17 +911,9 @@ def _add_create_options(parser):
help="Arbeitsspeicher des Snapshots laden") help="Arbeitsspeicher des Snapshots laden")
parser.add_argument("--no-resume", dest="resume", action="store_false", parser.add_argument("--no-resume", dest="resume", action="store_false",
help="kalt starten, auch wenn der Snapshot RAM enthaelt") 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=[], parser.add_argument("--transfer", action="append", metavar="NAME", default=[],
help="vorbereitetes Transfer-Laufwerk anhaengen " help="vorbereitetes Transfer-Laufwerk anhaengen "
"(mehrfach moeglich)") "(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("--memory", type=int, help="Arbeitsspeicher in MB abweichend")
parser.add_argument("--cores", type=int, help="Kerne abweichend") parser.add_argument("--cores", type=int, help="Kerne abweichend")
parser.add_argument("--name", help="Name der neuen Maschine") parser.add_argument("--name", help="Name der neuen Maschine")
@@ -1083,16 +938,16 @@ def build_parser():
pvesnap-recovery pvesnap-recovery
ncurses-Oberflaeche mit Uebersicht und Assistent ncurses-Oberflaeche mit Uebersicht und Assistent
pvesnap-recovery live 802 --exchange 20G pvesnap-recovery live 802 --transfer dumps
neuesten Snapshot von VM 802 ohne Netzwerk starten, mit einem 20-GB- neuesten Snapshot von VM 802 ohne Netzwerk starten, mit dem
Austauschlaufwerk fuer den Datenbank-Dump vorbereiteten Transfer-Laufwerk "dumps"
pvesnap-recovery live 100 --exchange ja pvesnap-recovery live 100 --transfer dumps --transfer werkzeuge
Container 100 abgeschottet starten; ein Host-Verzeichnis erscheint Container 100 abgeschottet starten, mit zwei Laufwerken
im Container unter %s
pvesnap-recovery live 100 --exchange-dir /mnt/nfs/dumps pvesnap-recovery
dasselbe, aber mit einem eigenen (auch eingehaengten) Verzeichnis Taste v: Transfer-Laufwerke anlegen und befuellen (Commander)
Taste e: im laufenden Betrieb auswerfen und wieder einklinken
pvesnap-recovery recover 802 autosnap-... --newid 9802 pvesnap-recovery recover 802 autosnap-... --newid 9802
mit Netzwerk und gleicher Identitaet - nur wenn das Original steht! mit Netzwerk und gleicher Identitaet - nur wenn das Original steht!
@@ -1102,7 +957,7 @@ def build_parser():
pvesnap-recovery destroy 9802 pvesnap-recovery destroy 9802
restlos verwerfen: Maschine weg, Klone weg, Schutz der Snapshots geloest 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", "--verbose", action="store_true")
parser.add_argument("-V", "--version", action="version", parser.add_argument("-V", "--version", action="version",
version="pvesnap-recovery %s" % __version__) version="pvesnap-recovery %s" % __version__)
@@ -1123,14 +978,6 @@ def build_parser():
sub = subs.add_parser(name, help=helptext) sub = subs.add_parser(name, help=helptext)
sub.add_argument("vmid", type=int) 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_ = subs.add_parser(
"flatten", help="vom Quell-Snapshot loesen - fuer den Dauerbetrieb") "flatten", help="vom Quell-Snapshot loesen - fuer den Dauerbetrieb")
flatten_.add_argument("vmid", type=int) flatten_.add_argument("vmid", type=int)
@@ -1170,8 +1017,6 @@ def main(argv=None):
"recover": lambda: cmd_create(proxmox, args, "recovery"), "recover": lambda: cmd_create(proxmox, args, "recovery"),
"list": lambda: cmd_list(proxmox, args), "list": lambda: cmd_list(proxmox, args),
"destroy": lambda: cmd_destroy(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), "flatten": lambda: cmd_flatten(proxmox, args),
"cleanup": lambda: cmd_cleanup(proxmox, args), "cleanup": lambda: cmd_cleanup(proxmox, args),
"console": lambda: cmd_console(proxmox, args), "console": lambda: cmd_console(proxmox, args),