From 91c23ce2c0ebec314ab4b5aa558a370b9a2414ea Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sun, 9 Aug 2026 10:32:29 +0200 Subject: [PATCH] SPICE und USB lassen sich nachtraeglich einstellen Bisher liess sich beides nur beim Anlegen waehlen. Wer erst spaeter merkt, dass er den Dongle braucht, musste die Maschine wegwerfen und neu bauen. Jetzt geht es in der Detailansicht auf Taste p, mit dem Ist-Zustand im Dialog. Was dabei geht und was nicht, ist am laufenden System durchgemessen: SPICE ein/aus Neustart noetig - die Grafikkarte laesst sich einer laufenden Maschine nicht unterschieben USB dazu/weg sofort, solange der SPICE-Kanal schon steht USB ohne SPICE schaltet SPICE mit ein, sonst waeren die Anschluesse wirkungslos Vor einem noetigen Neustart wird gewarnt, und bei einer Maschine mit geladenem Arbeitsspeicher besonders deutlich: der ist danach endgueltig weg, weil Proxmox ihn schon beim ersten Start freigibt. Ein zweiter startet kalt, offene Programme und ungespeicherte Daten sind verloren. Beim Abschalten wird der vga-Eintrag entfernt statt auf "std" gesetzt - dann steht die Maschine wieder genau so da wie vorher, ohne eine Zeile, die es nie gab. Co-Authored-By: Claude Opus 5 (1M context) --- pvesnap/recovery.py | 78 ++++++++++++++++++++++++++++++++++++++++++ pvesnap/recovery_ui.py | 67 ++++++++++++++++++++++++++++++++++-- 2 files changed, 143 insertions(+), 2 deletions(-) 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"]