"""pvesnap-recovery - einen Snapshot als Maschine starten. Ohne Argumente kommt die ncurses-Oberflaeche, alles geht aber auch direkt: pvesnap-recovery Uebersicht und Assistent pvesnap-recovery list was gerade laeuft pvesnap-recovery live 802 neuesten Snapshot ohne Netz starten pvesnap-recovery live 802 --transfer dumps pvesnap-recovery recover 802 mit Netz, gleiche Identitaet pvesnap-recovery console 9802 noVNC-Adresse anzeigen pvesnap-recovery Taste v: Transfer-Laufwerke pvesnap-recovery destroy 9802 restlos verwerfen """ from __future__ import annotations import argparse import curses import logging import os import sys import time from datetime import datetime from . import __version__ from .curses_util import (C_DIM, C_FOOTER, C_HEADER, C_MARK, C_OK, C_SEL, C_WARN, choose, confirm, fill, init_colors, is_enter, is_escape, keybar, message, prompt, put, read_key) from .proxmox import Proxmox, ProxmoxError, pvesh_available from . import transfer, transfer_ui from .recovery import (RecoveryError, Spec, create, destroy, eject_transfer, find_orphans, find_stale_protections, flatten, flatten_cost, flatten_hint, human_bytes, insert_transfer, linked_volumes, display_state, list_instances, node_address, plan, release_protections, remove_orphans, set_display, spice_file, start, transfer_state) from .snapfs import list_snapshots from .util import truncate log = logging.getLogger("pvesnap.recovery_ui") KEYS = [ ("Enter", "Details", "Detail"), ("n", "Neu", "Neu"), ("s", "Starten", "Start"), ("h", "Herunterfahren", "Stop"), ("x", "Verwerfen", "Verwerf."), ("v", "Laufwerke", "Laufw."), ("c", "Aufraeumen", "Aufr."), ("r", "Aktualisieren", "Aktual."), ("q", "Beenden", "Ende"), ] NET_CHOICES = ( ("", "automatisch (nach Modus)"), ("none", "keine Netzwerkkarte"), ("down", "Karte vorhanden, abgeklemmt"), ("on", "voll am Netz - Vorsicht!"), ) RESUME_CHOICES = ( (None, "wenn im Snapshot vorhanden"), (True, "ja, Arbeitsspeicher laden"), (False, "nein, kalt starten"), ) # --------------------------------------------------------------------------- # Kleine Bausteine # --------------------------------------------------------------------------- def _wait_screen(win, text, title="pvesnap-recovery"): height, width = win.getmaxyx() win.erase() fill(win, 0, " " + title, curses.color_pair(C_HEADER) | curses.A_BOLD) put(win, height // 2, max(2, (width - len(text)) // 2), text, curses.A_BOLD) win.refresh() def _pages(win, lines, title, footer="beliebige Taste = weiter"): """Einen Textblock anzeigen, bei Bedarf seitenweise.""" height, width = win.getmaxyx() rows = max(1, height - 4) top = 0 while True: win.erase() fill(win, 0, " " + title, curses.color_pair(C_HEADER) | curses.A_BOLD) for row, (text, attr) in enumerate(lines[top:top + rows]): put(win, row + 2, 2, truncate(text, width - 4), attr) more = " (%d-%d von %d)" % (top + 1, min(top + rows, len(lines)), len(lines)) \ if len(lines) > rows else "" # Eine Liste wird wie ueberall sonst auf die Breite umgebrochen; ein # fester Text wuerde auf schmalen Terminals hinten abgeschnitten. zeilen = keybar(width, footer) if isinstance(footer, list) else [footer] if more: # Die Seitenangabe gehoert ans Ende, egal welche Form die # Fusszeile hat - sonst weiss man bei langen Inhalten nicht, # dass es weitergeht. zeilen[-1] = zeilen[-1] + more for offset, zeile in enumerate(zeilen[-2:]): fill(win, height - len(zeilen[-2:]) + offset, " " + zeile, curses.color_pair(C_FOOTER)) win.refresh() key = read_key(win) if key == curses.KEY_DOWN and top + rows < len(lines): top += 1 elif key == curses.KEY_UP and top > 0: top -= 1 elif key == curses.KEY_NPAGE and top + rows < len(lines): top = min(len(lines) - rows, top + rows) elif key == curses.KEY_PPAGE: top = max(0, top - rows) else: return key def _plain(lines): return [(text, 0) for text in lines] class _Progress: """Fortschrittsanzeige waehrend des Anlegens.""" def __init__(self, win, title): self.win = win self.title = title self.lines = [] def __call__(self, text): self.lines.append(text) height, width = self.win.getmaxyx() self.win.erase() fill(self.win, 0, " " + self.title, curses.color_pair(C_HEADER) | curses.A_BOLD) visible = self.lines[-(height - 4):] for row, line in enumerate(visible): attr = curses.A_BOLD if line is visible[-1] else curses.color_pair(C_DIM) put(self.win, row + 2, 2, truncate(line, width - 4), attr) fill(self.win, height - 1, " bitte warten ...", curses.color_pair(C_FOOTER)) self.win.refresh() # --------------------------------------------------------------------------- # Auswahl # --------------------------------------------------------------------------- def _pick_guest(win, guests): entries = [(g, "%s %-6d %-24s %-8s %s" % ("LXC" if g.type == "lxc" else "VM ", g.vmid, truncate(g.name or "-", 24), g.status, g.node)) for g in guests] return choose(win, "Von welchem Gast? (Esc = zurueck)", entries) def _pick_snapshot(win, snapshots): entries = [] for snap in snapshots: when = (datetime.fromtimestamp(snap.snaptime).strftime("%d.%m.%Y %H:%M") if snap.snaptime else "?") ram = "RAM" if "vmstate" in (snap.description or "") else "" entries.append((snap.name, "%-34s %s %-4s %s" % (truncate(snap.name, 34), when, ram, truncate((snap.description or "").replace("\n", " "), 34)))) return choose(win, "Welcher Snapshot? (Esc = zurueck)", entries) # --------------------------------------------------------------------------- # Optionsmaske # --------------------------------------------------------------------------- def _fields(guest): """Die Eingabefelder - je nach Gasttyp etwas andere.""" rows = [ ("mode", "Betriebsart", "choice", (("live", "live - abgeschottet, ohne Netzwerk"), ("recovery", "recovery - mit Netz, gleiche Identitaet"))), ("net", "Netzwerk", "choice", NET_CHOICES), ] if guest.type == "qemu": rows.append(("resume", "Arbeitsspeicher", "choice", RESUME_CHOICES)) rows.append(("transfers", "Transfer-Laufwerke", "transfers", ())) if guest.type == "qemu": rows.append(("spice", "SPICE-Anzeige", "choice", ((False, "nein - nur noVNC im Browser"), (True, "ja - zusaetzlich remote-viewer")))) rows.append(("usb", "USB-Weiterleitung", "choice", ((0, "keine"), (1, "1 Anschluss"), (2, "2 Anschluesse"), (4, "4 Anschluesse")))) rows += [ ("newid", "Neue VMID", "text", ()), ("node", "Node", "text", ()), ("start", "Danach starten", "choice", ((True, "ja"), (False, "nein"))), ] return rows def _field_text(spec, key, kind, choices): value = getattr(spec, key) if kind == "choice": for candidate, label in choices: if candidate == value: return label return str(value) if key == "newid": return str(value) if value else "automatisch (naechste freie)" if key == "transfers": return transfer_ui.summary(value) if key == "node": return value or "wie das Original" return str(value or "") def _options(win, guest, spec): """Maske fuer die Einstellungen. True = anlegen, False = abbrechen.""" rows = _fields(guest) index = 0 while True: height, width = win.getmaxyx() win.erase() fill(win, 0, " Wiederherstellung einrichten - %s" % guest.label, curses.color_pair(C_HEADER) | curses.A_BOLD) for row, (key, label, kind, choices) in enumerate(rows): selected = row == index attr = curses.color_pair(C_SEL) if selected else 0 text = " %-26s %s" % (label + ":", _field_text(spec, key, kind, choices)) fill(win, row + 2, text.ljust(width - 2), attr) note = { "mode": "live = zum Hineinschauen. recovery = die Maschine wirklich " "wieder in Betrieb nehmen.", "net": "Ohne Netz kommt nichts hinein und nichts hinaus - dafuer ist " "das Austauschlaufwerk da.", "resume": "Snapshots mit Arbeitsspeicher laufen genau dort weiter, wo " "sie standen - ohne Crash-Recovery.", "transfers": "Vorbereitete Austauschmedien. Mit v anlegen und " "befuellen, mit Enter auswaehlen.", "spice": "Zweiter Zugang ueber remote-viewer. Achtung: vertraegt " "sich nicht mit geladenem Arbeitsspeicher.", "usb": "Ein USB-Geraet vom eigenen Rechner in den Gast reichen - " "etwa einen Dongle. Laeuft ueber SPICE, nicht ueber das Netz.", "newid": "VMID der neuen Maschine.", "node": "Auf welchem Node sie laufen soll.", "start": "Sofort hochfahren.", }.get(rows[index][0], "") for offset, line in enumerate(_wrap(note, width - 6)[:3]): put(win, len(rows) + 4 + offset, 3, line, curses.color_pair(C_DIM)) for offset, line in enumerate(keybar( width, [("Pfeile", "Auswaehlen", "Wahl"), ("Enter/Leer", "Aendern", "Aendern"), ("v", "Laufwerke verwalten", "Laufw."), ("F10", "Anlegen", "Anlegen"), ("q", "Abbrechen", "Abbr.")])): fill(win, height - 2 + offset, " " + line, curses.color_pair(C_FOOTER)) win.refresh() key = read_key(win) current_key, _label, kind, choices = rows[index] if key in ("v", "V"): # Von hier aus verwalten statt zwei Ebenen zurueckzugehen: wer # merkt, dass ihm ein Laufwerk fehlt, merkt es genau an dieser # Stelle. Danach steht der Balken auf der Auswahlzeile. transfer_ui.screen(win, browse=_browse_transfer) index = next((i for i, r in enumerate(rows) if r[2] == "transfers"), index) elif key == curses.KEY_UP: index = (index - 1) % len(rows) elif key == curses.KEY_DOWN: index = (index + 1) % len(rows) elif key in (curses.KEY_F10,) or key == "\x06": return True elif is_escape(key) or key == "q": return False elif is_enter(key) or key == " " or key in (curses.KEY_RIGHT, curses.KEY_LEFT): if kind == "transfers": spec.transfers = transfer_ui.choose(win, spec.transfers) else: _edit(win, spec, current_key, kind, choices, back=key == curses.KEY_LEFT) def _wrap(text, width): words, lines, current = str(text).split(), [], "" for word in words: if len(current) + len(word) + 1 > width: lines.append(current) current = word else: current = (current + " " + word).strip() if current: lines.append(current) return lines def _edit(win, spec, key, kind, choices, back=False): if kind == "choice": values = [c[0] for c in choices] try: position = values.index(getattr(spec, key)) except ValueError: position = 0 position = (position - 1 if back else position + 1) % len(values) setattr(spec, key, values[position]) if key == "mode": # Die Betriebsart gibt die Vorgabe fuer das Netz vor. spec.net = "" return label = {"newid": "Neue VMID (leer = automatisch)", "node": "Node (leer = wie das Original)"}.get(key, key) answer = prompt(win, label, str(getattr(spec, key) or "")) if answer is None: return if key == "newid": spec.newid = int(answer) if answer.isdigit() else 0 else: setattr(spec, key, answer) # --------------------------------------------------------------------------- # Zusammenfassung # --------------------------------------------------------------------------- def summary_lines(plan_): """Was passieren wird - als Liste von (Text, Attribut).""" guest, spec = plan_.guest, plan_.spec lines = [ ("Quelle: %s auf %s" % (guest.label, guest.node), 0), ("Snapshot: %s" % plan_.snapshot, 0), ("Neu: %s auf %s" % (plan_.label, plan_.node), curses.A_BOLD), ("Betriebsart: %s" % ("recovery - mit Netz, gleiche Identitaet" if spec.mode == "recovery" else "live - abgeschottet"), 0), ("", 0), ("Datentraeger (Copy-on-Write-Klone, das Original bleibt unberuehrt):", 0), ] for key, volid, size in plan_.disks: lines.append((" %-9s %s%s" % (key, volid, (" %s" % size) if size else ""), 0)) lines.append(("", 0)) netzwerk = {"none": "keine Netzwerkkarte", "down": "Karte vorhanden, Leitung abgeklemmt", "on": "AM NETZ - mit den MAC-Adressen des Originals"} lines.append(("Netzwerk: %s" % netzwerk.get(plan_.net_mode, plan_.net_mode), curses.color_pair(C_WARN) if plan_.net_mode == "on" else 0)) if plan_.nets: for key, value in plan_.nets: lines.append((" %-9s %s" % (key, truncate(value, 60)), 0)) if plan_.vmstate: lines.append(("Arbeitsspeicher: %s" % ("wird geladen (%s) - laeuft weiter statt zu booten" % (human_bytes(plan_.vmstate_bytes) or "?") if plan_.resume else "vorhanden, wird aber nicht geladen"), curses.color_pair(C_OK) if plan_.resume else 0)) if plan_.spice: lines.append(("Anzeige: noVNC und SPICE (vga: qxl)", curses.color_pair(C_OK))) if plan_.usb: lines.append(("USB: %d Weiterleitung(en) ueber SPICE" % plan_.usb, curses.color_pair(C_OK))) if plan_.iso: lines.append(("CD: %s" % plan_.iso, 0)) for volume in plan_.transfers: lines.append(("Transfer: %-14s %8s im Gast als %s" % (volume.name, human_bytes(volume.size), volume.label), curses.color_pair(C_OK))) if plan_.notes: lines.append(("", 0)) for note in plan_.notes: for line in _wrap("- " + note, 74): lines.append((line, curses.color_pair(C_DIM))) for entry in plan_.warnings + plan_.critical: lines.append(("", 0)) for line in _wrap("!! " + entry, 74): lines.append((line, curses.color_pair(C_WARN) | curses.A_BOLD)) return lines # --------------------------------------------------------------------------- # Assistent # --------------------------------------------------------------------------- def _new(stdscr, proxmox): _wait_screen(stdscr, "Frage Proxmox nach VMs und Containern ...") try: guests = proxmox.inventory(refresh=True) except ProxmoxError as exc: message(stdscr, str(exc), error=True) return while True: guest = _pick_guest(stdscr, guests) if guest is None: return _wait_screen(stdscr, "Lese Snapshots von %s ..." % guest.label) try: snapshots = [s for s in list_snapshots(proxmox, guest) if s.complete] except ProxmoxError as exc: message(stdscr, str(exc), error=True) continue if not snapshots: message(stdscr, "%s hat keine brauchbaren Snapshots." % guest.label, error=True) continue snapname = _pick_snapshot(stdscr, snapshots) if snapname is None: continue spec = Spec() while True: if not _options(stdscr, guest, spec): break _wait_screen(stdscr, "Pruefe, was noetig ist ...") try: plan_ = plan(proxmox, guest, snapname, spec) except (RecoveryError, ProxmoxError) as exc: _pages(stdscr, [(line, curses.color_pair(C_WARN)) for line in _wrap(str(exc), 74)], "Geht so nicht") continue key = _pages(stdscr, summary_lines(plan_), "Zusammenfassung - noch ist nichts passiert", "j = anlegen | beliebige andere Taste = zurueck") if key not in ("j", "J", "y", "Y"): continue if plan_.critical: # Beim Namen nennen, worum es geht - "Warnungen gelesen?" # klickt man weg, ohne hinzusehen. if not confirm(stdscr, truncate(plan_.critical[0], 60) + " Trotzdem?"): continue elif plan_.warnings and not confirm( stdscr, "Warnungen gelesen und trotzdem anlegen?"): continue progress = _Progress(stdscr, "Richte %s ein" % plan_.label) try: instance = create(proxmox, plan_, progress=progress) except (RecoveryError, ProxmoxError, OSError) as exc: _pages(stdscr, [(line, curses.color_pair(C_WARN)) for line in _wrap(str(exc), 74)] + [("", 0)] + _plain(progress.lines), "Fehlgeschlagen") break if spec.start: progress("Starte %s ..." % instance.label) try: hint = start(proxmox, instance, progress=progress) except (ProxmoxError, RecoveryError) as exc: message(stdscr, "Angelegt, aber nicht gestartet: %s" % exc, error=True) else: if hint: _pages(stdscr, [(line, curses.color_pair(C_WARN)) for line in _wrap(hint, 74)], "Angelegt und gestartet - mit einem Hinweis") _details(stdscr, proxmox, instance) return # --------------------------------------------------------------------------- # Detailansicht # --------------------------------------------------------------------------- def _details(stdscr, proxmox, instance): while True: status = proxmox.guest_status(instance.guest) running = status.get("status") == "running" lines = [ ("Maschine: %s auf %s" % (instance.label, instance.node), curses.A_BOLD), ("Zustand: %s" % (status.get("status") or "unbekannt"), curses.color_pair(C_OK if running else C_DIM)), ("Herkunft: %s" % instance.origin, 0), ("Betriebsart: %s" % instance.mode, 0), ("Angelegt: %s" % (datetime.fromtimestamp(instance.created) .strftime("%d.%m.%Y %H:%M") if instance.created else "?"), 0), ("", 0), ("Konsole im Browser:", 0), ] for line in _wrap(instance.console_url(), 74): lines.append((" " + line, curses.color_pair(C_MARK))) if instance.type == "lxc": lines.append((" oder auf dem Host: pct enter %d" % instance.vmid, curses.color_pair(C_MARK))) else: lines.append((" SPICE (remote-viewer): pvesnap-recovery spice %d" % instance.vmid, curses.color_pair(C_MARK))) if instance.resumed: lines.append(("", 0)) lines.append(("Der Arbeitsspeicher des Snapshots wurde geladen - beim " "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): linked = [] lines.append(("", 0)) if linked: lines.append(("Datentraeger: Linked Clone - haengt am Quell-Snapshot", curses.color_pair(C_MARK))) lines.append((" Belegt nur, was seither geschrieben wurde. Der " "Quell-Snapshot ist dafuer unloeschbar.", curses.color_pair(C_DIM))) lines.append((" Fuer den Dauerbetrieb mit f loesen (kopiert die " "Daten wirklich).", curses.color_pair(C_DIM))) elif instance.volumes: lines.append(("Datentraeger: eigenstaendig - haengt an keinem Snapshot", curses.color_pair(C_OK))) # "Auswerfen" allein verschweigt die Haelfte - dieselbe Taste holt das # Laufwerk auch wieder herein. Und v gehoert hierher, weil ein # ausgeworfenes Laufwerk beim Host liegt und genau jetzt befuellt # werden will. footer = [("s", "Starten", "Start"), ("h", "Herunterfahren", "Stop"), ("e", "Auswerfen/Einklinken", "Ausw."), ("v", "Laufwerke", "Laufw."), ("p", "SPICE/USB", "SPICE"), ("f", "Loesen", "Loesen"), ("x", "Verwerfen", "Verw."), ("q", "zurueck", "zurueck")] key = _pages(stdscr, lines, "Wiederherstellung %s" % instance.label, footer) if key in ("q", "\x1b") or is_escape(key): return if key in ("s", "S"): _do(stdscr, "Starte %s ..." % instance.label, lambda: start(proxmox, instance)) elif key in ("h", "H"): _herunterfahren(stdscr, proxmox, instance) elif key in ("e", "E"): _swap(stdscr, proxmox, instance, steckplaetze) elif key in ("v", "V"): transfer_ui.screen(stdscr, browse=_browse_transfer) elif key in ("p", "P"): _display(stdscr, proxmox, instance) elif key in ("f", "F"): _flatten(stdscr, proxmox, instance, linked) elif key in ("x", "X"): if _destroy(stdscr, proxmox, instance): return def _herunterfahren(stdscr, proxmox, instance): """Sauber herunterfahren - und bei Widerstand nicht einfach zuschlagen.""" _wait_screen(stdscr, "Fahre %s herunter ..." % instance.label) try: proxmox.shutdown_guest(instance.guest) return except (ProxmoxError, RecoveryError) as exc: grund = str(exc) weg = choose(stdscr, "%s faehrt nicht herunter" % instance.label, [ ("warten", "Im Gast selbst herunterfahren - pvesnap wartet"), ("hart", "Hart ausschalten - wie Stecker ziehen"), (None, "Abbrechen, laeuft weiter"), ]) if weg == "warten": if _warte_bis_aus(stdscr, proxmox, instance): message(stdscr, "%s ist aus." % instance.label) return if weg == "hart": if confirm(stdscr, "Wirklich hart ausschalten? Daten koennen verloren gehen."): _do(stdscr, "Schalte %s aus ..." % instance.label, lambda: proxmox.stop_guest(instance.guest)) return message(stdscr, grund, error=True) def _do(stdscr, text, action): _wait_screen(stdscr, text) try: hint = action() except (ProxmoxError, RecoveryError) as exc: message(stdscr, str(exc), error=True) return if isinstance(hint, str) and hint: message(stdscr, hint, error=True) def _warte_bis_aus(stdscr, proxmox, instance, minuten=30): """Warten, bis der Gast von innen heruntergefahren ist. Der verlaesslichere Weg als ACPI: Windows ignoriert den Aus-Knopf gern, wenn eine Anwendung den Herunterfahrvorgang blockiert oder der Shutdown Event Tracker nach einem Grund fragt. Wer im Gast selbst herunterfaehrt, umgeht das - und pvesnap sieht am Zustand, wann es soweit ist. """ curses.halfdelay(20) # get_wch wartet hoechstens 2s begonnen = time.time() try: while time.time() - begonnen < minuten * 60: try: zustand = proxmox.guest_status(instance.guest).get("status") or "?" except ProxmoxError: zustand = "?" if zustand != "running": return True height, width = stdscr.getmaxyx() stdscr.erase() fill(stdscr, 0, " Warte auf das Herunterfahren von %s" % instance.label, curses.color_pair(C_HEADER) | curses.A_BOLD) zeilen = [ "Bitte jetzt IM GAST herunterfahren.", "", " Windows Start -> Ein/Aus -> Herunterfahren", " Linux poweroff bzw. shutdown -h now", "", "Sobald die Maschine aus ist, startet pvesnap sie von selbst", "wieder - dann mit der neuen Ausstattung.", "", "Zustand: %s seit %d:%02d" % (zustand, int(time.time() - begonnen) // 60, int(time.time() - begonnen) % 60), ] for zeile, text in enumerate(zeilen): put(stdscr, zeile + 2, 3, truncate(text, width - 6), curses.A_BOLD if zeile == 0 else 0) fill(stdscr, height - 1, " q oder Esc = Abbrechen (die Maschine " "laeuft dann einfach weiter)", curses.color_pair(C_FOOTER)) stdscr.refresh() key = read_key(stdscr) if key in ("q", "Q") or is_escape(key): return False finally: curses.cbreak() # halbe Verzoegerung zuruecknehmen return False def _neu_starten(stdscr, proxmox, instance): """Neustart, damit vorgemerkte Aenderungen wirksam werden.""" weg = choose(stdscr, "Wie soll %s neu starten?" % instance.label, [ ("warten", "Ich fahre im Gast herunter - pvesnap wartet und startet dann"), ("acpi", "Proxmox herunterfahren lassen (ACPI) - Windows blockt das oft"), (None, "Gar nicht - spaeter selbst"), ]) if weg is None: message(stdscr, "Eingetragen - wirksam beim naechsten Start.") return if weg == "acpi": _do(stdscr, "Fahre %s herunter und wieder hoch ..." % instance.label, lambda: proxmox.reboot_guest(instance.guest)) return if not _warte_bis_aus(stdscr, proxmox, instance): message(stdscr, "Abgebrochen - die Aenderung greift beim naechsten " "Start. Dann hier mit s starten.") return _do(stdscr, "Ist aus - starte %s wieder ..." % instance.label, lambda: start(proxmox, instance)) def _display(stdscr, proxmox, instance): """SPICE-Anzeige und USB-Weiterleitung nachtraeglich einstellen.""" if instance.type != "qemu": message(stdscr, "Bei Containern gibt es kein SPICE - das ist eine " "reine Sache virtueller Maschinen.", error=True) return spice, usb = display_state(proxmox, instance) ziel = choose(stdscr, "SPICE-Anzeige (jetzt: %s)" % ("ein" if spice else "aus"), [(True, "ein - zusaetzlich remote-viewer, noetig fuer USB"), (False, "aus - nur noVNC im Browser")]) if ziel is None: return anzahl = 0 if ziel: anzahl = choose(stdscr, "USB-Weiterleitung (jetzt: %d)" % usb, [(0, "keine"), (1, "1 Anschluss"), (2, "2 Anschluesse"), (4, "4 Anschluesse")]) if anzahl is None: return laeuft = proxmox.guest_status(instance.guest).get("status") == "running" if laeuft and ziel != spice: # Die Grafikkarte laesst sich nur beim Start wechseln. Und der # geladene Arbeitsspeicher ist danach weg - Proxmox gibt ihn schon # beim ersten Start frei, ein zweiter startet also kalt. warnung = ["Dafuer muss %s neu starten - die Grafikkarte laesst sich " "im Betrieb nicht wechseln." % instance.label, "", "Gleich kannst du waehlen, wie: entweder du faehrst im Gast " "selbst herunter und pvesnap startet ihn danach wieder, oder " "Proxmox versucht es per ACPI. Der erste Weg ist der " "verlaesslichere - Windows ignoriert den Aus-Knopf gern.", "", "Ein Neustart von innen im Gast wuerde nichts bringen - dabei " "setzt sich nur die Maschine zurueck, der QEMU-Prozess laeuft " "weiter mit der alten Grafikkarte."] if instance.resumed: warnung.append("") warnung.append("ACHTUNG: der geladene Arbeitsspeicher ist danach " "endgueltig weg. Die Maschine bootet kalt, offene " "Programme und ungespeicherte Daten sind verloren.") key = _pages(stdscr, [(line, curses.color_pair(C_WARN) | curses.A_BOLD) for eintrag in warnung for line in _wrap(eintrag, 74)], "Neustart noetig", "j = trotzdem | andere Taste = abbrechen") if key not in ("j", "J", "y", "Y"): return progress = _Progress(stdscr, "Stelle Anzeige um") try: geaendert, neustart = set_display(proxmox, instance, ziel, anzahl, progress=progress) except (RecoveryError, ProxmoxError) as exc: _pages(stdscr, [(line, curses.color_pair(C_WARN)) for line in _wrap(str(exc), 74)], "Fehlgeschlagen") return if not geaendert: message(stdscr, "War schon so eingestellt.") return if neustart: _neu_starten(stdscr, proxmox, instance) else: message(stdscr, "Uebernommen.") 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 - durchgereichte " "Verzeichnisse bleiben, wie sie sind.", 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) return _wait_screen(stdscr, "Ermittle, wieviel kopiert werden muss ...") try: cost = flatten_cost(proxmox, instance) except (RecoveryError, ProxmoxError): cost = 0 if not confirm(stdscr, "%s kopieren und dauerhaft loesen?" % (human_bytes(cost) if cost else "%d Datentraeger" % len(linked))): return progress = _Progress(stdscr, "Loese %s vom Quell-Snapshot" % instance.label) try: done, _already, other = flatten(proxmox, instance, progress=progress) except (RecoveryError, ProxmoxError) as exc: _pages(stdscr, [(line, curses.color_pair(C_WARN)) for line in _wrap(str(exc), 74)] + [("", 0)] + _plain(progress.lines), "Fehlgeschlagen") return lines = ["%d Datentraeger geloest." % len(done), ""] for volid in other: lines += _wrap("%s: %s" % (volid, flatten_hint(proxmox, volid)), 74) if not linked_volumes(proxmox, instance): lines += ["", "%s ist jetzt eigenstaendig. Die Snapshots des Originals " "lassen sich wieder loeschen." % instance.label] _pages(stdscr, _plain(lines), "Fertig") def _destroy(stdscr, proxmox, instance): if not confirm(stdscr, "%s wirklich verwerfen? Alle Aenderungen darin sind weg." % instance.label): return False progress = _Progress(stdscr, "Verwerfe %s" % instance.label) try: destroy(proxmox, instance, progress=progress) except (RecoveryError, ProxmoxError) as exc: _pages(stdscr, [(line, curses.color_pair(C_WARN)) for line in _wrap(str(exc), 74)] + [("", 0)] + _plain(progress.lines), "Fehlgeschlagen") return False return True # --------------------------------------------------------------------------- # Uebersicht # --------------------------------------------------------------------------- def _overview(stdscr, proxmox): index = 0 instances = [] reload_ = True while True: if reload_: _wait_screen(stdscr, "Frage nach vorhandenen Wiederherstellungen ...") instances = list_instances(proxmox) reload_ = False index = min(index, max(0, len(instances) - 1)) height, width = stdscr.getmaxyx() stdscr.erase() fill(stdscr, 0, " pvesnap-recovery %s - Snapshots als Maschine starten" % __version__, curses.color_pair(C_HEADER) | curses.A_BOLD) if not instances: put(stdscr, 3, 3, "Zurzeit laeuft keine Wiederherstellung.", curses.A_BOLD) put(stdscr, 5, 3, "Mit n eine neue anlegen:", 0) put(stdscr, 6, 5, "live Snapshot abgeschottet starten, ohne Netzwerk -", curses.color_pair(C_DIM)) put(stdscr, 7, 5, " zum Hineinschauen und Daten herausholen.", curses.color_pair(C_DIM)) put(stdscr, 8, 5, "recovery Snapshot mit Netzwerk und gleicher Identitaet", curses.color_pair(C_DIM)) put(stdscr, 9, 5, " wieder in Betrieb nehmen.", curses.color_pair(C_DIM)) else: put(stdscr, 2, 2, "%-10s %-9s %-22s %-28s %s" % ("Maschine", "Zustand", "Node", "Herkunft", "Modus"), curses.A_BOLD | curses.A_UNDERLINE) rows = max(1, height - 6) top = max(0, min(index - rows + 1, len(instances) - rows)) for row in range(min(rows, len(instances) - top)): instance = instances[top + row] attr = curses.color_pair(C_SEL) if top + row == index else 0 if not attr and instance.status == "running": attr = curses.color_pair(C_OK) text = " %-10s %-9s %-22s %-28s %s" % ( instance.label, instance.status or "?", truncate(instance.node or "?", 22), truncate(instance.origin, 28), instance.mode) fill(stdscr, row + 3, text, attr) for offset, line in enumerate(keybar(width, KEYS)): fill(stdscr, height - 2 + offset, " " + line, curses.color_pair(C_FOOTER)) stdscr.refresh() key = read_key(stdscr) current = instances[index] if instances else None if key in ("q", "Q") or is_escape(key): return 0 if key in ("n", "N"): _new(stdscr, proxmox) reload_ = True elif key in ("v", "V"): transfer_ui.screen(stdscr, browse=_browse_transfer) reload_ = True elif key in ("c", "C"): _cleanup(stdscr, proxmox) reload_ = True elif key in ("r", "R"): reload_ = True elif key == curses.KEY_UP and instances: index = (index - 1) % len(instances) elif key == curses.KEY_DOWN and instances: index = (index + 1) % len(instances) elif current is None: continue elif is_enter(key): _details(stdscr, proxmox, current) reload_ = True elif key in ("s", "S"): _do(stdscr, "Starte %s ..." % current.label, lambda: start(proxmox, current)) reload_ = True elif key in ("h", "H"): _herunterfahren(stdscr, proxmox, current) reload_ = True elif key in ("x", "X"): _destroy(stdscr, proxmox, current) reload_ = True def _cleanup(stdscr, proxmox): """Reste aufraeumen, die entstehen, wenn jemand an pvesnap vorbei loescht. Zwei verschiedene Dinge, deshalb zwei Durchgaenge: liegengebliebene Datentraeger - und geschuetzte Snapshots, deren Klon es nicht mehr gibt. Den zweiten Fall findet die Suche nach Datentraegern prinzipiell nicht. """ _wait_screen(stdscr, "Suche liegengebliebene Datentraeger ...") try: orphans = find_orphans(proxmox) except (RecoveryError, ProxmoxError) as exc: message(stdscr, str(exc), error=True) return fortschritt = _Progress(stdscr, "Suche geschuetzte Snapshots ohne Klon") try: stale = find_stale_protections(proxmox, progress=fortschritt) except (RecoveryError, ProxmoxError) as exc: message(stdscr, str(exc), error=True) return klone = [e for e in orphans if e["parent"]] if not klone and not stale: message(stdscr, "Nichts liegengeblieben - alles sauber.") return zeilen = [] if klone: zeilen.append(("Datentraeger, zu denen es keinen Gast mehr gibt:", curses.A_BOLD)) for entry in klone: zeilen.append((" %-26s %8s Klon von %s" % (entry["volid"], human_bytes(entry["bytes"]), entry["parent"]), curses.color_pair(C_MARK))) zeilen.append(("", 0)) if stale: zeilen.append(("Geschuetzte Snapshots, an denen kein Klon mehr haengt:", curses.A_BOLD)) for entry in stale: zeilen.append((" %s@%s" % (entry["volid"], entry["snapshot"]), curses.color_pair(C_MARK))) zeilen.append(("", 0)) for text in _wrap("An diesen scheitert die Vorhaltezeit dauerhaft mit " "\"snapshot is protected\". Der Schutz wird geloest - " "geloescht wird dabei nichts.", 74): zeilen.append((text, curses.color_pair(C_DIM))) zeilen.append(("", 0)) ohne_eltern = [e for e in orphans if not e["parent"]] if ohne_eltern: for text in _wrap("Ausserdem liegen %d Datentraeger ohne Elternteil herum. " "Die werden hier nicht angefasst - dafuer gibt es " "'pvesnap-recovery cleanup --all'." % len(ohne_eltern), 74): zeilen.append((text, curses.color_pair(C_DIM))) taste = _pages(stdscr, zeilen, "Aufraeumen", "j = aufraeumen | andere Taste = nichts tun") if taste not in ("j", "J", "y", "Y"): return fortschritt = _Progress(stdscr, "Raeume auf") bericht = [] if klone: entfernt, gescheitert = remove_orphans(proxmox, klone, progress=fortschritt) bericht.append("%d Datentraeger entfernt." % len(entfernt)) bericht += [" fehlgeschlagen: %s" % p for p in gescheitert] if stale: geloest, gescheitert = release_protections(proxmox, stale, progress=fortschritt) bericht.append("%s geloest." % _anzahl(len(geloest), "Schutzmarke", "Schutzmarken")) bericht += [" fehlgeschlagen: %s" % p for p in gescheitert] _pages(stdscr, _plain(bericht), "Fertig") def _browse_transfer(win, path, volume): """Den Zwei-Fenster-Commander auf ein Transfer-Laufwerk loslassen. Links das Laufwerk, rechts der lokale Rechner - also genau der Weg, um Werkzeuge hineinzukopieren, bevor die Maschine startet, und den Dump hinterher wieder herauszuholen. """ from .explorer import Explorer, Pane links = Pane(path=path, root=path, readonly=False, title="Transfer: %s (%s)" % (volume.name, volume.label)) rechts = Pane(path=os.getcwd(), root="/", readonly=False, title="Lokaler Rechner") Explorer(links, rechts, subtitle="Transfer-Laufwerk %s - beide Seiten beschreibbar" % volume.name).run(win) def _main(stdscr, _args): curses.curs_set(0) if hasattr(curses, "set_escdelay"): curses.set_escdelay(25) init_colors() stdscr.keypad(True) return _overview(stdscr, Proxmox()) # --------------------------------------------------------------------------- # Befehlszeile # --------------------------------------------------------------------------- def _find_guest(proxmox, vmid): guest = next((g for g in proxmox.inventory() if g.vmid == int(vmid)), None) if guest is None: raise RecoveryError("Gast %s gibt es nicht." % vmid) return guest def _find_instance(proxmox, vmid): instance = next((i for i in list_instances(proxmox) if i.vmid == int(vmid)), None) if instance is None: raise RecoveryError("%s ist keine von pvesnap angelegte Wiederherstellung. " "Vorhandene zeigt: pvesnap-recovery list" % vmid) return instance def _newest_snapshot(proxmox, guest): snapshots = [s for s in list_snapshots(proxmox, guest) if s.complete] if not snapshots: raise RecoveryError("%s hat keine brauchbaren Snapshots." % guest.label) return snapshots[0].name def _spec_from_args(args, mode): spec = Spec(mode=mode) spec.newid = args.newid or 0 spec.node = args.node or "" spec.net = args.net or "" spec.resume = args.resume spec.transfers = list(args.transfer or []) spec.spice = bool(getattr(args, "spice", False)) spec.usb = int(getattr(args, "usb", 0) or 0) spec.iso = getattr(args, "iso", "") or "" spec.memory = args.memory or 0 spec.cores = args.cores or 0 spec.name = args.name or "" spec.keep_binds = args.keep_binds spec.start = not args.no_start return spec def _print_summary(plan_): print(" Quelle : %s auf %s" % (plan_.guest.label, plan_.guest.node)) print(" Snapshot : %s" % plan_.snapshot) print(" Neu : %s auf %s" % (plan_.label, plan_.node)) for key, volid, size in plan_.disks: print(" Klon : %-9s %s%s" % (key, volid, (" %s" % size) if size else "")) netzwerk = {"none": "keine Netzwerkkarte", "down": "Karte vorhanden, Leitung abgeklemmt", "on": "AM NETZ mit den MAC-Adressen des Originals"} print(" Netzwerk : %s" % netzwerk.get(plan_.net_mode, plan_.net_mode)) if plan_.resume: print(" Arbeitsspei.: wird geladen (%s)" % (human_bytes(plan_.vmstate_bytes) or "?")) for volume in plan_.transfers: print(" Transfer : %-14s %8s im Gast als %s" % (volume.name, human_bytes(volume.size), volume.label)) for note in plan_.notes: print(" Hinweis : %s" % note) for warning in plan_.warnings: print(" ACHTUNG : %s" % warning) for reason in plan_.critical: print(" GEFAHR : %s" % reason) def _ask(question): """Frage am Terminal. Ohne Eingabemoeglichkeit gilt: nein. Wird die Ausgabe in ein Skript geleitet, liefert input() ein EOFError - das darf keinen Stacktrace geben, sondern muss als Absage zaehlen. """ try: return input(question).strip().lower() except (EOFError, KeyboardInterrupt): print() return "" def cmd_create(proxmox, args, mode): guest = _find_guest(proxmox, args.vmid) snapname = args.snapshot or _newest_snapshot(proxmox, guest) spec = _spec_from_args(args, mode) plan_ = plan(proxmox, guest, snapname, spec) print("Vorhaben:") _print_summary(plan_) # Gefaehrliches laesst sich nicht mit -y wegwinken. -y heisst "keine # Routinefragen", nicht "frag mich auch dann nicht, wenn es weh tut". if plan_.critical and not args.force: # Sonst ueberholt die Fehlerausgabe die gepufferte Zusammenfassung, # sobald jemand die Ausgabe umleitet - und die Begruendung stuende # ueber dem, worauf sie sich bezieht. sys.stdout.flush() print("\nDas geht so nicht:", file=sys.stderr) for reason in plan_.critical: print(" - %s" % reason, file=sys.stderr) if args.yes: print("\n-y reicht dafuer nicht. Entweder das Original stoppen oder\n" "bewusst --force setzen.", file=sys.stderr) return 2 print("\nWer das trotzdem will, tippt jetzt das Wort ja aus.") if _ask("Trotzdem anlegen? [ja/nein] ") not in ("ja", "yes"): print("Abgebrochen.") return 1 elif not args.yes: if plan_.warnings: print("\nEs gibt Warnungen - bitte oben lesen.") answer = _ask("\nAnlegen? [j/N] ") if answer not in ("j", "y", "ja", "yes"): print("Abgebrochen.") return 1 instance = create(proxmox, plan_, progress=lambda text: print(" " + text)) print("\n%s ist eingerichtet." % instance.label) if spec.start: print(" Starte ...") hint = start(proxmox, instance, progress=lambda text: print(" " + text)) print(" laeuft." if not hint else " ACHTUNG: %s" % hint) _print_access(instance) return 0 def _print_access(instance): print("\n Konsole im Browser:") print(" %s" % instance.console_url()) if instance.type == "lxc": print(" oder auf dem Host: pct enter %d" % instance.vmid) print("\n Wieder verwerfen: pvesnap-recovery destroy %d" % instance.vmid) def cmd_list(proxmox, _args): instances = list_instances(proxmox) if not instances: print("Zurzeit laeuft keine Wiederherstellung.") return 0 print("%-10s %-9s %-14s %-30s %s" % ("Maschine", "Zustand", "Node", "Herkunft", "Modus")) for instance in instances: print("%-10s %-9s %-14s %-30s %s" % (instance.label, instance.status or "?", instance.node or "?", instance.origin, instance.mode)) return 0 def cmd_destroy(proxmox, args): instance = _find_instance(proxmox, args.vmid) if not args.yes: print("%s (%s) wird restlos entfernt - alle Aenderungen darin sind weg." % (instance.label, instance.origin)) if _ask("Wirklich? [j/N] ") not in ("j", "y", "ja", "yes"): print("Abgebrochen.") return 1 destroy(proxmox, instance, progress=lambda text: print(" " + text)) 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): """Zwei Durchgaenge - sie finden verschiedene Dinge. Der erste sucht Datentraeger ohne Gast. Der zweite geschuetzte Snapshots ohne Klon: die findet der erste prinzipiell nicht, weil es dazu keinen Datentraeger mehr gibt, nach dem er suchen koennte. """ schlecht = _cleanup_volumes(proxmox, args) print() schlecht += _cleanup_protections(proxmox, args) return 1 if schlecht else 0 def _cleanup_volumes(proxmox, args): """Liegengebliebene Datentraeger. Liefert die Zahl der Fehlschlaege.""" orphans = find_orphans(proxmox) if not orphans: print("Keine liegengebliebenen Datentraeger.") return 0 clones = [e for e in orphans if e["parent"]] plain = [e for e in orphans if not e["parent"]] print("Datentraeger, zu denen es keinen Gast mehr gibt:\n") for entry in clones: print(" %-26s %8s Klon von %s" % (entry["volid"], human_bytes(entry["bytes"]), entry["parent"])) for entry in plain: print(" %-26s %8s kein Klon" % (entry["volid"], human_bytes(entry["bytes"]))) wanted = orphans if args.all else clones if not wanted: print("\nNur Datentraeger ohne Elternteil gefunden. Die werden nicht von\n" "selbst entfernt - mit --all auch diese.") return 0 print("\nEntfernt werden %d Datentraeger%s." % (len(wanted), "" if args.all else " (nur Klone; --all nimmt auch die anderen)")) print("Der Schutz der Quell-Snapshots wird dabei geloest, sofern kein\n" "anderer Klon mehr daran haengt.") if not args.yes: if _ask("\nEntfernen? [j/N] ") not in ("j", "y", "ja", "yes"): print("Abgebrochen.") return 1 removed, failed = remove_orphans(proxmox, wanted, progress=lambda text: print(" " + text)) print("\n%d entfernt." % len(removed)) for problem in failed: print(" fehlgeschlagen: %s" % problem, file=sys.stderr) return 1 if failed else 0 def _anzahl(zahl, einzahl, mehrzahl): return "%d %s" % (zahl, einzahl if zahl == 1 else mehrzahl) def _cleanup_protections(proxmox, args): """Geschuetzte Snapshots ohne Klon. Liefert die Zahl der Fehlschlaege.""" print("Suche geschuetzte Snapshots ohne Klon ...") stale = find_stale_protections(proxmox, progress=lambda text: print(" " + text)) if not stale: print("Keine gefunden.") return 0 print("\nGeschuetzte Snapshots, an denen kein Klon mehr haengt:\n") for entry in stale: print(" %s@%s" % (entry["volid"], entry["snapshot"])) print("\nAn diesen scheitert die Vorhaltezeit dauerhaft mit\n" "\"snapshot is protected\". Der Schutz wird geloest - geloescht wird\n" "dabei nichts, die Snapshots bleiben stehen.") if not args.yes: if _ask("\nSchutz loesen? [j/N] ") not in ("j", "y", "ja", "yes"): print("Abgebrochen.") return 0 released, failed = release_protections( proxmox, stale, progress=lambda text: print(" " + text)) print("\n%s geloest." % _anzahl(len(released), "Schutzmarke", "Schutzmarken")) for problem in failed: print(" fehlgeschlagen: %s" % problem, file=sys.stderr) return 1 if failed else 0 def cmd_spice(proxmox, args): instance = _find_instance(proxmox, args.vmid) text = spice_file(proxmox, instance) if args.output: with open(args.output, "w", encoding="utf-8") as handle: handle.write(text) os.chmod(args.output, 0o600) print(args.output) print("\n Auf dem eigenen Rechner oeffnen:", file=sys.stderr) print(" scp root@%s:%s . && remote-viewer %s" % (node_address(instance.node), args.output, os.path.basename(args.output)), file=sys.stderr) else: sys.stdout.write(text) print("\n Umleiten und mit remote-viewer oeffnen:", file=sys.stderr) print(" pvesnap-recovery spice %d -o vm.vv" % instance.vmid, file=sys.stderr) print(" Das Kennwort darin ist ein Einmal-Kennwort und laeuft bald ab.", file=sys.stderr) return 0 def cmd_console(proxmox, args): instance = _find_instance(proxmox, args.vmid) print(instance.console_url()) if instance.type == "lxc": print("pct enter %d" % instance.vmid, file=sys.stderr) return 0 def cmd_start(proxmox, args): hint = start(proxmox, _find_instance(proxmox, args.vmid), progress=lambda text: print(" " + text)) print("laeuft." if not hint else "ACHTUNG: %s" % hint) return 0 def cmd_stop(proxmox, args): proxmox.shutdown_guest(_find_instance(proxmox, args.vmid).guest) print("heruntergefahren.") return 0 def _add_create_options(parser): parser.add_argument("vmid", type=int, help="VMID des Originals") parser.add_argument("snapshot", nargs="?", help="Name des Snapshots (Vorgabe: der neueste)") parser.add_argument("--newid", type=int, help="VMID der neuen Maschine") parser.add_argument("--node", help="Node, auf dem sie laufen soll") parser.add_argument("--net", choices=("none", "down", "on"), help="none = keine Karte, down = Karte ohne Leitung, " "on = voll am Netz") parser.add_argument("--resume", dest="resume", action="store_true", default=None, help="Arbeitsspeicher des Snapshots laden") parser.add_argument("--no-resume", dest="resume", action="store_false", help="kalt starten, auch wenn der Snapshot RAM enthaelt") parser.add_argument("--spice", action="store_true", help="SPICE-Anzeige einschalten (vga: qxl) - vertraegt " "sich nicht mit geladenem Arbeitsspeicher") parser.add_argument("--usb", type=int, default=0, metavar="ANZAHL", help="so viele USB-Weiterleitungen ueber SPICE, z.B. " "fuer einen Dongle (geht auch mit RAM-Zustand)") parser.add_argument("--iso", metavar="VOLID", help="Abbild als CD einlegen, z.B. local:iso/virtio-win.iso") parser.add_argument("--transfer", action="append", metavar="NAME", default=[], help="vorbereitetes Transfer-Laufwerk anhaengen " "(mehrfach moeglich)") parser.add_argument("--memory", type=int, help="Arbeitsspeicher in MB abweichend") parser.add_argument("--cores", type=int, help="Kerne abweichend") parser.add_argument("--name", help="Name der neuen Maschine") parser.add_argument("--keep-binds", action="store_true", help="durchgereichte Host-Verzeichnisse des Originals " "uebernehmen (Vorgabe: weglassen)") parser.add_argument("--no-start", action="store_true", help="nur einrichten, nicht starten") parser.add_argument("-y", "--yes", action="store_true", help="Routinefragen ueberspringen (deckt --force nicht ab)") parser.add_argument("--force", action="store_true", help="auch dann anlegen, wenn das Original noch laeuft - " "gleiche MAC und IP zweimal im Netz") def build_parser(): parser = argparse.ArgumentParser( prog="pvesnap-recovery", formatter_class=argparse.RawDescriptionHelpFormatter, description="Einen Proxmox-Snapshot als eigenstaendige Maschine starten.", epilog="""Beispiele: pvesnap-recovery ncurses-Oberflaeche mit Uebersicht und Assistent pvesnap-recovery live 802 --transfer dumps neuesten Snapshot von VM 802 ohne Netzwerk starten, mit dem vorbereiteten Transfer-Laufwerk "dumps" pvesnap-recovery live 100 --transfer dumps --transfer werkzeuge Container 100 abgeschottet starten, mit zwei Laufwerken pvesnap-recovery Taste v: Transfer-Laufwerke anlegen und befuellen (Commander) Taste e: im laufenden Betrieb auswerfen und wieder einklinken pvesnap-recovery recover 802 autosnap-... --newid 9802 mit Netzwerk und gleicher Identitaet - nur wenn das Original steht! pvesnap-recovery pull 9802 Austauschlaufwerk nach dem Herunterfahren auf den Host holen pvesnap-recovery destroy 9802 restlos verwerfen: Maschine weg, Klone weg, Schutz der Snapshots geloest """) parser.add_argument("-v", "--verbose", action="store_true") parser.add_argument("-V", "--version", action="version", version="pvesnap-recovery %s" % __version__) subs = parser.add_subparsers(dest="command", metavar="BEFEHL") live = subs.add_parser("live", help="Snapshot abgeschottet starten (ohne Netz)") _add_create_options(live) recover = subs.add_parser("recover", help="Snapshot mit Netz und gleicher Identitaet starten") _add_create_options(recover) subs.add_parser("list", help="vorhandene Wiederherstellungen zeigen") for name, helptext in (("start", "hochfahren"), ("stop", "herunterfahren"), ("console", "noVNC-Adresse zeigen")): sub = subs.add_parser(name, help=helptext) sub.add_argument("vmid", type=int) spice = subs.add_parser("spice", help="SPICE-Verbindungsdatei fuer remote-viewer") spice.add_argument("vmid", type=int) spice.add_argument("-o", "--output", metavar="DATEI", help="hineinschreiben statt auf die Ausgabe") flatten_ = subs.add_parser( "flatten", help="vom Quell-Snapshot loesen - fuer den Dauerbetrieb") flatten_.add_argument("vmid", type=int) flatten_.add_argument("-y", "--yes", action="store_true", help="nicht nachfragen") flatten_.add_argument("--force", action="store_true", help="auch starten, wenn der Storage dafuer zu voll ist") cleanup = subs.add_parser( "cleanup", help="Klone entfernen, zu denen es keinen Gast mehr gibt") cleanup.add_argument("--all", action="store_true", help="auch Datentraeger ohne Elternteil entfernen") cleanup.add_argument("-y", "--yes", action="store_true", help="nicht nachfragen") destroy_ = subs.add_parser("destroy", help="Wiederherstellung restlos verwerfen") destroy_.add_argument("vmid", type=int) destroy_.add_argument("-y", "--yes", action="store_true", help="nicht nachfragen") return parser def main(argv=None): args = build_parser().parse_args(argv) logging.basicConfig(level=logging.DEBUG if args.verbose else logging.WARNING, format="%(levelname)s %(message)s") if not pvesh_available(): print("'pvesh' nicht gefunden - pvesnap-recovery laeuft auf dem " "Proxmox-VE-Host.", file=sys.stderr) return 3 if os.geteuid() != 0: print("pvesnap-recovery braucht root-Rechte (Klone anlegen, Maschinen " "einrichten).", file=sys.stderr) return 4 proxmox = Proxmox() handlers = { "live": lambda: cmd_create(proxmox, args, "live"), "recover": lambda: cmd_create(proxmox, args, "recovery"), "list": lambda: cmd_list(proxmox, args), "destroy": lambda: cmd_destroy(proxmox, args), "flatten": lambda: cmd_flatten(proxmox, args), "cleanup": lambda: cmd_cleanup(proxmox, args), "console": lambda: cmd_console(proxmox, args), "spice": lambda: cmd_spice(proxmox, args), "start": lambda: cmd_start(proxmox, args), "stop": lambda: cmd_stop(proxmox, args), } try: if args.command in handlers: return handlers[args.command]() except (RecoveryError, ProxmoxError) as exc: print("Fehler: %s" % exc, file=sys.stderr) return 1 except KeyboardInterrupt: print("\nAbgebrochen.", file=sys.stderr) return 130 # Ohne Befehl: Oberflaeche. Dabei darf nichts ins Terminal protokolliert werden. logging_handlers = logging.getLogger().handlers[:] if not args.verbose: logging.getLogger().handlers = [] try: return curses.wrapper(_main, args) except KeyboardInterrupt: return 130 finally: logging.getLogger().handlers = logging_handlers if __name__ == "__main__": # pragma: no cover sys.exit(main())