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:
co-authored by
Claude Opus 5
parent
0a92ec0984
commit
f2de07ecfd
@@ -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
@@ -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
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user