diff --git a/pvesnap/recovery.py b/pvesnap/recovery.py index e5ef171..b0cba1f 100644 --- a/pvesnap/recovery.py +++ b/pvesnap/recovery.py @@ -1242,6 +1242,84 @@ def spice_file(proxmox, instance): return "\n".join(lines) + "\n" +USB_KEY = re.compile(r"^usb\d+$") + + +def display_state(proxmox, instance): + """(SPICE an?, Anzahl USB-Weiterleitungen) aus der Konfiguration.""" + try: + config = proxmox.guest_config(instance.guest) + except ProxmoxError: + return False, 0 + spice = str(config.get("vga") or "").startswith("qxl") + usb = sum(1 for key, value in config.items() + if USB_KEY.match(key) and str(value).strip().lower() == "spice") + return spice, usb + + +def set_display(proxmox, instance, spice, usb, progress=None): + """SPICE-Anzeige und USB-Weiterleitung nachtraeglich einstellen. + + Liefert (geaendert, neustart_noetig). Die Anzeige selbst laesst sich nur + beim Start festlegen - qxl bringt eine andere Grafikkarte mit, die kann + man einer laufenden Maschine nicht unterschieben. USB-Anschluesse dagegen + sind steckbar, solange der SPICE-Kanal schon steht. + """ + def step(text): + log.info("%s", text) + if progress: + progress(text) + + if instance.type != "qemu": + raise RecoveryError("SPICE und USB-Weiterleitung gibt es nur bei " + "virtuellen Maschinen, nicht bei Containern.") + usb = max(0, min(14, int(usb or 0))) + if usb and not spice: + spice = True + step("SPICE wird mit eingeschaltet - ohne den Kanal waeren die " + "Anschluesse wirkungslos") + + config = proxmox.guest_config(instance.guest) + hat_spice = str(config.get("vga") or "").startswith("qxl") + vorhanden = sorted(key for key, value in config.items() + if USB_KEY.match(key) and str(value).strip().lower() == "spice") + + aenderungen = {} + entfernen = [] + if spice and not hat_spice: + aenderungen["vga"] = "qxl" + elif not spice and hat_spice: + # Eintrag entfernen statt auf "std" setzen: dann steht die Maschine + # wieder genau so da wie vorher, ohne eine Zeile, die es nie gab. + entfernen.append("vga") + entfernen.extend(vorhanden) # ohne Kanal sind sie sinnlos + vorhanden = [] + + if spice: + for _ in range(usb - len(vorhanden)): + slot = _free_usb_slot(dict(config, **aenderungen)) + aenderungen[slot] = "spice" + config = dict(config, **{slot: "spice"}) + entfernen.extend(vorhanden[usb:]) + + if not aenderungen and not entfernen: + return False, False + + if entfernen: + step("Entferne %s" % ", ".join(entfernen)) + proxmox.set_guest_config(instance.guest, {"delete": ",".join(entfernen)}) + if aenderungen: + step("Setze %s" % ", ".join("%s=%s" % kv for kv in sorted(aenderungen.items()))) + proxmox.set_guest_config(instance.guest, aenderungen) + + # Die Grafikkarte wechselt erst beim naechsten Start. USB-Anschluesse + # stecken sofort, aber nur wenn der SPICE-Kanal schon lief. + laeuft = proxmox.guest_status(instance.guest).get("status") == "running" + neustart = laeuft and ("vga" in aenderungen or "vga" in entfernen + or (spice and not hat_spice)) + return True, neustart + + def _default_name(guest, plan_): base = re.sub(r"[^A-Za-z0-9-]", "-", guest.name or "vm%d" % guest.vmid)[:40] return ("%s-wdh" % base).strip("-") diff --git a/pvesnap/recovery_ui.py b/pvesnap/recovery_ui.py index 3790eed..3d8129a 100644 --- a/pvesnap/recovery_ui.py +++ b/pvesnap/recovery_ui.py @@ -30,7 +30,8 @@ from . import transfer, transfer_ui from .recovery import (RecoveryError, Spec, create, destroy, eject_transfer, find_orphans, flatten, flatten_cost, flatten_hint, human_bytes, insert_transfer, linked_volumes, - list_instances, node_address, plan, remove_orphans, + display_state, list_instances, node_address, plan, + remove_orphans, set_display, spice_file, start, transfer_state) from .snapfs import list_snapshots from .util import truncate @@ -526,7 +527,7 @@ def _details(stdscr, proxmox, instance): # ausgeworfenes Laufwerk beim Host liegt und genau jetzt befuellt # werden will. footer = ("s Start | h Stop | e Auswerfen/Einklinken | v Laufwerke | " - "f Loesen | x Verwerfen | q zurueck") + "p SPICE/USB | f Loesen | x Verwerfen | q zurueck") key = _pages(stdscr, lines, "Wiederherstellung %s" % instance.label, footer) if key in ("q", "\x1b") or is_escape(key): @@ -541,6 +542,8 @@ def _details(stdscr, proxmox, instance): _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"): @@ -559,6 +562,66 @@ def _do(stdscr, text, action): message(stdscr, hint, error=True) +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 = usb + 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] + if instance.resumed: + warnung.append("ACHTUNG: der geladene Arbeitsspeicher ist danach " + "endgueltig weg. Die Maschine bootet kalt, offene " + "Programme und ungespeicherte Daten sind verloren.") + warnung.append("Im Gast vorher sauber herunterfahren, was noch laeuft.") + 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 and confirm(stdscr, "%s jetzt neu starten?" % instance.label): + _do(stdscr, "Starte %s neu ..." % instance.label, + lambda: (proxmox.stop_guest(instance.guest), + start(proxmox, instance))[1]) + elif neustart: + message(stdscr, "Eingetragen - wirksam beim naechsten Start.") + 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"]