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) <noreply@anthropic.com>
This commit is contained in:
duffyduck
2026-08-09 10:32:29 +02:00
co-authored by Claude Opus 5
parent 56003a71ab
commit 91c23ce2c0
2 changed files with 143 additions and 2 deletions
+78
View File
@@ -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("-")
+65 -2
View File
@@ -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"]