diff --git a/pvesnap/recovery.py b/pvesnap/recovery.py index 727528a..728a60f 100644 --- a/pvesnap/recovery.py +++ b/pvesnap/recovery.py @@ -955,16 +955,25 @@ def create(proxmox, plan_, progress=None): if guest.type == "lxc": # Beim Container wird der Mountpunkt des Hosts durchgereicht. # Ein Bind-Mount ist dasselbe Dateisystem an zwei Stellen, kein - # zweites Einhaengen - das ist unbedenklich. + # zweites Einhaengen - Host und Gast koennen beide arbeiten. step("Reiche Transfer-Laufwerk %r durch" % volume.name) path = transfer.host_mount(volume.name) slot = _free_mp_slot(new_config) - new_config[slot] = "%s,mp=/mnt/%s" % (path, volume.name) + value = "%s,mp=/mnt/%s" % (path, volume.name) + new_config[slot] = value + attached.append({"name": volume.name, "key": slot, "drive": value, + "kind": "bind", "pending": False}) else: step("Haenge Transfer-Laufwerk %r an" % volume.name) slot = _free_disk_slot(new_config) - new_config[slot] = transfer.drive_string(volume.name) - attached.append(volume.name) + value = transfer.drive_string(volume.name) + # Mit geladenem Arbeitsspeicher wacht der Gast in einem Zustand + # auf, in dem es die Platte nicht gab - dann muss sie erst nach + # dem Fortsetzen dazukommen, sonst bemerkt er sie nie. + if not plan_.resume: + new_config[slot] = value + attached.append({"name": volume.name, "key": slot, "drive": value, + "kind": "disk", "pending": bool(plan_.resume)}) # -- restliche Anpassungen ----------------------------------------- if spec.memory: @@ -1071,10 +1080,32 @@ def start(proxmox, instance, progress=None): def _attach_pending(proxmox, instance, step=None): - """Das Austauschlaufwerk an die schon laufende Maschine anstecken.""" + """Was erst nach dem Fortsetzen dazukommen darf, jetzt anstecken.""" + notes = [] + for entry in _transfer_entries(instance): + if not entry.get("pending"): + continue + if step: + step("Stecke Transfer-Laufwerk %r als %s an" + % (entry["name"], entry["key"])) + try: + proxmox.set_guest_config(instance.guest, {entry["key"]: entry["drive"]}) + config = proxmox.guest_config(instance.guest) + except ProxmoxError as exc: + notes.append("Transfer-Laufwerk %r liess sich nicht anstecken: %s" + % (entry["name"], exc)) + continue + if entry["key"] not in config: + notes.append("Transfer-Laufwerk %r ist eingetragen, aber nicht " + "angesteckt - es erscheint erst nach einem Neustart." + % entry["name"]) + else: + entry["pending"] = False + _registry_add(instance) + exchange = instance.exchange or {} if not exchange.get("pending"): - return "" + return " ".join(notes) key, drive = exchange.get("key"), exchange.get("drive") if not key or not drive: return "" @@ -1084,16 +1115,114 @@ def _attach_pending(proxmox, instance, step=None): proxmox.set_guest_config(instance.guest, {key: drive}) config = proxmox.guest_config(instance.guest) except ProxmoxError as exc: - return "Das Austauschlaufwerk liess sich nicht anstecken: %s" % 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. - return ("Das Austauschlaufwerk ist eingetragen, aber nicht angesteckt " - "worden (Hotplug fuer Platten ist bei dieser Maschine aus). " - "Es erscheint erst nach einem Neustart des Gastes.") - exchange["pending"] = False + 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) + + +def _transfer_entries(instance): + """Die Transfer-Laufwerke einer Instanz als Datensaetze. + + Aeltere Merklisten enthalten nur Namen - die werden hier mitgelesen. + """ + entries = [] + for entry in instance.transfers or []: + if isinstance(entry, dict): + entries.append(entry) + else: + entries.append({"name": str(entry), "key": "", "drive": "", + "kind": "disk", "pending": False}) + return entries + + +def transfer_state(proxmox, instance): + """[(Datensatz, eingesteckt?)] - was gerade in der Maschine steckt.""" + try: + config = proxmox.guest_config(instance.guest) + except ProxmoxError: + config = {} + return [(entry, bool(entry.get("key") and entry["key"] in config)) + for entry in _transfer_entries(instance)] + + +def eject_transfer(proxmox, instance, name, progress=None): + """Ein Transfer-Laufwerk bei laufender Maschine abziehen. + + Danach gehoert es wieder dem Host: einhaengen, befuellen, auslesen - und + mit insert_transfer() wieder hineingeben, ohne die Maschine anzufassen. + + Wichtig: im Gast vorher aushaengen. Proxmox meldet das Geraet ab, aber ein + Dateisystem, das noch beschrieben wird, nimmt das uebel. + """ + def step(text): + log.info("%s", text) + if progress: + progress(text) + + entry = next((e for e in _transfer_entries(instance) if e["name"] == name), None) + if entry is None: + raise RecoveryError("%r haengt nicht an %s." % (name, instance.label)) + if entry.get("kind") == "bind": + raise RecoveryError( + "Bei Containern gibt es nichts auszuwerfen: das Verzeichnis ist " + "durchgereicht, Host und Gast sehen dieselben Dateien sofort.") + if not entry.get("key"): + raise RecoveryError("Zu %r ist kein Steckplatz vermerkt." % name) + + step("Werfe %r aus (%s)" % (name, entry["key"])) + try: + proxmox.set_guest_config(instance.guest, {"delete": entry["key"]}) + except ProxmoxError as exc: + raise RecoveryError( + "%r liess sich nicht abziehen: %s\n" + "Meist haelt der Gast es noch - dort erst aushaengen " + "(Linux: umount, Windows: Auswerfen im Explorer)." % (name, exc)) + + entry["pending"] = True _registry_add(instance) - return "" + transfer.release(name) + step("%r gehoert jetzt wieder dem Host" % name) + return True + + +def insert_transfer(proxmox, instance, name, progress=None): + """Ein zuvor ausgeworfenes Transfer-Laufwerk wieder einklinken.""" + def step(text): + log.info("%s", text) + if progress: + progress(text) + + entry = next((e for e in _transfer_entries(instance) if e["name"] == name), None) + if entry is None: + raise RecoveryError("%r gehoert nicht zu %s." % (name, instance.label)) + if entry.get("kind") == "bind": + raise RecoveryError("Bei Containern ist das Verzeichnis dauerhaft " + "durchgereicht - nichts einzuklinken.") + + volume = transfer.get(name) + if volume.mountpoint: + step("%r ist noch auf dem Host eingehaengt - haenge aus" % name) + transfer.host_umount(name) + + drive = transfer.drive_string(name) + entry["drive"] = drive + step("Klinke %r wieder ein (%s)" % (name, entry["key"])) + try: + proxmox.set_guest_config(instance.guest, {entry["key"]: drive}) + except ProxmoxError as exc: + raise RecoveryError("%r liess sich nicht einklinken: %s" % (name, exc)) + entry["pending"] = False + _registry_add(instance) + return True def _default_name(guest, plan_): @@ -1495,7 +1624,7 @@ def destroy(proxmox, instance, progress=None, keep_snapshot_protection=False): except RecoveryError as exc: log.warning("Schutz von %s@%s blieb: %s", volid, snapname, exc) - for name in instance.transfers or []: + for name in [e["name"] for e in _transfer_entries(instance)]: step("Gebe Transfer-Laufwerk %r wieder frei" % name) try: transfer.host_umount(name) diff --git a/pvesnap/recovery_ui.py b/pvesnap/recovery_ui.py index 32e7ccb..7ab4e6e 100644 --- a/pvesnap/recovery_ui.py +++ b/pvesnap/recovery_ui.py @@ -27,6 +27,7 @@ from .curses_util import (C_DIM, C_FOOTER, C_HEADER, C_MARK, C_OK, C_SEL, C_WARN keybar, message, prompt, put, read_key) from .proxmox import Proxmox, ProxmoxError, pvesh_available from .recovery import (EXCHANGE_GUEST_PATH, RecoveryError, Spec, create, + eject_transfer, insert_transfer, transfer_state, destroy, find_orphans, flatten, flatten_cost, flatten_hint, human_bytes, linked_volumes, list_instances, plan, pull, release, @@ -476,6 +477,24 @@ def _details(stdscr, proxmox, instance): "ersten Start lief die Maschine genau weiter.", curses.color_pair(C_DIM))) + steckplaetze = transfer_state(proxmox, instance) + if steckplaetze: + lines.append(("", 0)) + lines.append(("Transfer-Laufwerke:", 0)) + for entry, drin in steckplaetze: + if entry.get("kind") == "bind": + zustand = "durchgereicht (Host und Gast gleichzeitig)" + farbe = C_OK + else: + zustand = ("eingesteckt als %s" % entry["key"]) if drin \ + else "ausgeworfen - gehoert dem Host" + farbe = C_OK if drin else C_MARK + lines.append((" %-14s %s" % (entry["name"], zustand), + curses.color_pair(farbe))) + if any(e.get("kind") != "bind" for e, _ in steckplaetze): + lines.append((" e = auswerfen / einklinken, im laufenden " + "Betrieb", curses.color_pair(C_DIM))) + try: linked = linked_volumes(proxmox, instance) except (RecoveryError, ProxmoxError): @@ -493,7 +512,7 @@ def _details(stdscr, proxmox, instance): lines.append(("Datentraeger: eigenstaendig - haengt an keinem Snapshot", curses.color_pair(C_OK))) - footer = ("s Starten | h Herunterfahren | a Austausch | f Loesen | " + footer = ("s Start | h Stop | e Auswerfen | a Austausch | f Loesen | " "x Verwerfen | q zurueck") key = _pages(stdscr, lines, "Wiederherstellung %s" % instance.label, footer) @@ -507,6 +526,8 @@ def _details(stdscr, proxmox, instance): lambda: proxmox.shutdown_guest(instance.guest)) elif key in ("a", "A"): _pull(stdscr, proxmox, instance) + elif key in ("e", "E"): + _swap(stdscr, proxmox, instance, steckplaetze) elif key in ("f", "F"): _flatten(stdscr, proxmox, instance, linked) elif key in ("x", "X"): @@ -545,6 +566,35 @@ def _pull(stdscr, proxmox, instance): ]), "Austauschlaufwerk") +def _swap(stdscr, proxmox, instance, steckplaetze): + """Transfer-Laufwerk im laufenden Betrieb abziehen oder wieder einklinken.""" + wechselbar = [(e, drin) for e, drin in steckplaetze if e.get("kind") != "bind"] + if not wechselbar: + message(stdscr, "Hier gibt es nichts zu wechseln.", error=True) + return + + if len(wechselbar) == 1: + entry, drin = wechselbar[0] + else: + gewaehlt = choose(stdscr, "Welches Laufwerk?", + [((e, d), "%-14s %s" % (e["name"], + "eingesteckt" if d else "ausgeworfen")) + for e, d in wechselbar]) + if gewaehlt is None: + return + entry, drin = gewaehlt + + name = entry["name"] + if drin: + if not confirm(stdscr, "%r auswerfen? Im Gast vorher aushaengen!" % name): + return + _do(stdscr, "Werfe %r aus ..." % name, + lambda: eject_transfer(proxmox, instance, name)) + else: + _do(stdscr, "Klinke %r ein ..." % name, + lambda: insert_transfer(proxmox, instance, name)) + + def _flatten(stdscr, proxmox, instance, linked): if not linked: message(stdscr, "%s haengt an keinem Snapshot mehr." % instance.label)