SPICE-Anzeige und USB-Weiterleitung fuer Wiederherstellungen

Der Anlass: ein Software-Schutzmodul am USB-Port. Normalerweise haengt so ein
Dongle an einem USB-Server im Netz - eine abgeschottete Wiederherstellung hat
aber keins. Ueber SPICE kommt das Geraet vom Rechner des Bedieners, nicht ueber
das Gastnetz. Die Maschine bleibt also ohne Netzwerk und hat trotzdem ihren
Dongle.

  --spice        SPICE-Anzeige (vga: qxl), zusaetzlich zu noVNC
  --usb <n>      so viele USB-Weiterleitungen
  --iso <volid>  Abbild als CD einlegen
  spice <vmid>   Verbindungsdatei fuer remote-viewer (.vv)

Zwei Dinge zeigte erst der Test, beide haetten die Sache still unbrauchbar
gemacht:

  vga: qxl zerstoert den geladenen Arbeitsspeicher. qxl hat 64 MB
  Grafikspeicher, die Vorgabe 16 - beim Laden kommt "Size mismatch:
  vga.vram", und die Maschine bleibt angehalten stehen. Wieder mit TASK OK.
  Wird jetzt abgelehnt, mit den drei Auswegen in der Meldung. Der dauerhafte
  ist, am Original 'vga: qxl' zu setzen - dann tragen alle kuenftigen
  Snapshots es mit und beides geht gleichzeitig.

  USB ohne SPICE ist wirkungslos. QEMU legt die usbredir-Geraete zwar an, aber
  ohne SPICE-Server gibt es keinen Kanal dafuer - spiceproxy sagt schlicht "no
  spice port". --usb schaltet die Anzeige deshalb mit ein.

Ausserdem korrigiert: in der .vv-Datei darf das Zertifikat nicht in echte
Zeilenumbrueche uebersetzt werden. virt-viewer erwartet die Zeichenfolge \n als
zwei Zeichen in einer einzigen Zeile; sonst steht nur die erste hinter "ca=".

tools/get-guest-tools.sh holt virtio-win auf einen ISO-Storage. Fuer den
Dongle wird davon nichts gebraucht - die Tools sind nur fuer Zwischenablage,
Aufloesung und Mauszeiger.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
duffyduck
2026-08-03 12:52:00 +02:00
co-authored by Claude Opus 5
parent a1076c6641
commit e88a80cb25
3 changed files with 290 additions and 1 deletions
+124
View File
@@ -444,6 +444,9 @@ class Spec:
net: str = "" # leer = Vorgabe des Modus; none|down|on net: str = "" # leer = Vorgabe des Modus; none|down|on
resume: object = None # None = RAM-Zustand nehmen, wenn vorhanden resume: object = None # None = RAM-Zustand nehmen, wenn vorhanden
transfers: list = field(default_factory=list) # Namen vorhandener Laufwerke transfers: list = field(default_factory=list) # Namen vorhandener Laufwerke
spice: bool = False # SPICE-Anzeige (vga: qxl)
usb: int = 0 # so viele USB-Weiterleitungen fuer SPICE
iso: str = "" # Abbild als CD einlegen (volid)
memory: int = 0 # 0 = wie im Snapshot memory: int = 0 # 0 = wie im Snapshot
cores: int = 0 cores: int = 0
name: str = "" name: str = ""
@@ -470,6 +473,9 @@ class Plan:
vmstate_bytes: int = 0 vmstate_bytes: int = 0
resume: bool = False resume: bool = False
transfers: list = field(default_factory=list) # [transfer.Volume] transfers: list = field(default_factory=list) # [transfer.Volume]
spice: bool = False
usb: int = 0
iso: str = ""
warnings: list = field(default_factory=list) warnings: list = field(default_factory=list)
# Was nicht nur unschoen, sondern gefaehrlich ist. Hierfuer genuegt ein # Was nicht nur unschoen, sondern gefaehrlich ist. Hierfuer genuegt ein
# beilaeufiges "ja" nicht - siehe cmd_create() und die Oberflaeche. # beilaeufiges "ja" nicht - siehe cmd_create() und die Oberflaeche.
@@ -740,6 +746,54 @@ def plan(proxmox, guest, snapname, spec):
% (name, volume.state_text)) % (name, volume.state_text))
result.transfers.append(volume) result.transfers.append(volume)
# -- SPICE, USB-Weiterleitung, CD --------------------------------------
result.usb = max(0, int(spec.usb or 0))
result.spice = bool(spec.spice)
result.iso = spec.iso or ""
if result.usb > 14:
raise RecoveryError("Hoechstens 14 USB-Weiterleitungen.")
# USB-Weiterleitung laeuft ueber den SPICE-Kanal. Ohne SPICE legt QEMU die
# Geraete zwar an, aber es gibt keine Verbindung, die sie transportiert -
# "no spice port". Also gehoert beides zusammen.
if result.usb and not result.spice:
result.spice = True
result.notes.append(
"SPICE-Anzeige wird mit eingeschaltet: die USB-Weiterleitung laeuft "
"ueber den SPICE-Kanal, ohne den waeren die Anschluesse wirkungslos.")
original_vga = str(config.get("vga") or "")
if result.spice and result.resume and not original_vga.startswith("qxl"):
raise RecoveryError(
"SPICE und geladener Arbeitsspeicher gehen bei dieser Maschine "
"nicht zusammen.\n"
"SPICE verlangt 'vga: qxl' (64 MB Grafikspeicher), im Snapshot "
"steckt die Vorgabe mit 16 MB. Der gespeicherte Zustand passt dann "
"nicht mehr und die Maschine bleibt beim Laden haengen "
"('Size mismatch: vga.vram').\n"
"\n"
"Moeglichkeiten:\n"
" --no-resume kalt starten, dafuer mit SPICE und USB\n"
" ohne --spice warmer RAM-Zustand, dafuer nur noVNC\n"
"\n"
"Dauerhaft loesen laesst sich das nur am Original: steht dort "
"'vga: qxl', tragen alle kuenftigen Snapshots das mit, und beides "
"geht gleichzeitig.")
if result.spice:
result.notes.append(
"Zugang auf zwei Wegen: noVNC im Browser und SPICE ueber "
"'pvesnap-recovery spice %d' (remote-viewer)." % result.newid)
if result.usb:
result.notes.append(
"Im remote-viewer unter 'USB-Geraeteauswahl' das Geraet anhaken - "
"es kommt vom eigenen Rechner, nicht ueber das Netz des Gastes. "
"Guest-Tools braucht es dafuer nicht.")
if result.iso:
result.notes.append("Eingelegt wird %s - im Gast als CD-Laufwerk."
% result.iso)
# -- Hinweise ---------------------------------------------------------- # -- Hinweise ----------------------------------------------------------
if result.dropped and not spec.keep_binds: if result.dropped and not spec.keep_binds:
result.notes.append("Nicht uebernommen werden Einbindungen von Host-" result.notes.append("Nicht uebernommen werden Einbindungen von Host-"
@@ -874,6 +928,20 @@ def create(proxmox, plan_, progress=None):
attached.append({"name": volume.name, "key": slot, "drive": value, attached.append({"name": volume.name, "key": slot, "drive": value,
"kind": "disk", "pending": bool(plan_.resume)}) "kind": "disk", "pending": bool(plan_.resume)})
# -- SPICE, USB-Weiterleitung, CD ----------------------------------
if plan_.spice:
step("Schalte SPICE-Anzeige ein (vga: qxl)")
new_config["vga"] = "qxl"
for _ in range(plan_.usb):
slot = _free_usb_slot(new_config)
new_config[slot] = "spice"
if plan_.usb:
step("%d USB-Weiterleitung(en) fuer SPICE eingerichtet" % plan_.usb)
if plan_.iso:
slot = _free_cdrom_slot(new_config)
new_config[slot] = "%s,media=cdrom" % plan_.iso
step("Lege %s als CD ein (%s)" % (plan_.iso, slot))
# -- restliche Anpassungen ----------------------------------------- # -- restliche Anpassungen -----------------------------------------
if spec.memory: if spec.memory:
new_config["memory"] = spec.memory new_config["memory"] = spec.memory
@@ -1101,6 +1169,41 @@ def insert_transfer(proxmox, instance, name, progress=None):
return True return True
# Die Reihenfolge ist die einer .vv-Datei fuer remote-viewer; "ca" muss ganz
# zum Schluss, weil das Zertifikat mehrzeilig ist.
_VV_KEYS = ("type", "host", "port", "tls-port", "password", "proxy",
"host-subject", "title", "toggle-fullscreen", "release-cursor",
"secure-attention", "delete-this-file", "ca")
def spice_file(proxmox, instance):
"""Verbindungsbeschreibung fuer remote-viewer (.vv) als Text.
Proxmox liefert die Angaben als JSON; daraus wird hier die Datei gebaut,
die virt-viewer erwartet. Sie enthaelt ein Einmal-Kennwort und laeuft nach
kurzer Zeit ab - also frisch erzeugen, wenn sie nicht mehr zieht.
"""
data = proxmox._json(["create", "/nodes/%s/%s/%d/spiceproxy"
% (instance.node, instance.type, instance.vmid)])
if not isinstance(data, dict) or not data.get("host"):
raise RecoveryError(
"%s liefert keine SPICE-Verbindung. Laeuft die Maschine, und ist "
"die SPICE-Anzeige eingeschaltet (vga: qxl)?" % instance.label)
lines = ["[virt-viewer]"]
for key in _VV_KEYS:
if key in data and data[key] not in (None, ""):
# Bewusst ohne Umwandlung: das Zertifikat enthaelt die Zeichenfolge
# \n als zwei Zeichen, und genau so erwartet virt-viewer sie. Wuerde
# man daraus echte Zeilenumbrueche machen, stuende nur die erste
# Zeile hinter "ca=" und der Rest waere kaputt.
lines.append("%s=%s" % (key, data[key]))
for key in sorted(data):
if key not in _VV_KEYS and data[key] not in (None, ""):
lines.append("%s=%s" % (key, data[key]))
return "\n".join(lines) + "\n"
def _default_name(guest, plan_): def _default_name(guest, plan_):
base = re.sub(r"[^A-Za-z0-9-]", "-", guest.name or "vm%d" % guest.vmid)[:40] base = re.sub(r"[^A-Za-z0-9-]", "-", guest.name or "vm%d" % guest.vmid)[:40]
return ("%s-wdh" % base).strip("-") return ("%s-wdh" % base).strip("-")
@@ -1132,6 +1235,27 @@ def _free_disk_slot(config):
raise RecoveryError("Kein freier Platz fuer ein Austauschlaufwerk.") raise RecoveryError("Kein freier Platz fuer ein Austauschlaufwerk.")
def _free_usb_slot(config):
for index in range(0, 14):
key = "usb%d" % index
if key not in config:
return key
raise RecoveryError("Alle 14 USB-Anschluesse sind belegt.")
def _free_cdrom_slot(config):
"""Ein CD-Laufwerk - moeglichst das leere, das schon da ist."""
for key in sorted(config):
if QEMU_DISK_KEY.match(key) and "media=cdrom" in str(config[key]) \
and str(config[key]).split(",")[0] in ("none", "cdrom"):
return key
for index in range(0, 4):
key = "ide%d" % index
if key not in config:
return key
raise RecoveryError("Kein Platz fuer ein CD-Laufwerk frei.")
def _free_mp_slot(config): def _free_mp_slot(config):
for index in range(0, 256): for index in range(0, 256):
if "mp%d" % index not in config: if "mp%d" % index not in config:
+64 -1
View File
@@ -27,7 +27,8 @@ from .curses_util import (C_DIM, C_FOOTER, C_HEADER, C_MARK, C_OK, C_SEL, C_WARN
keybar, message, prompt, put, read_key) keybar, message, prompt, put, read_key)
from .proxmox import Proxmox, ProxmoxError, pvesh_available from .proxmox import Proxmox, ProxmoxError, pvesh_available
from .recovery import (RecoveryError, Spec, create, from .recovery import (RecoveryError, Spec, create,
eject_transfer, insert_transfer, transfer_state, eject_transfer, insert_transfer, spice_file,
transfer_state,
destroy, find_orphans, flatten, flatten_cost, flatten_hint, destroy, find_orphans, flatten, flatten_cost, flatten_hint,
human_bytes, linked_volumes, list_instances, plan, human_bytes, linked_volumes, list_instances, plan,
remove_orphans, start) remove_orphans, start)
@@ -165,6 +166,13 @@ def _fields(guest):
if guest.type == "qemu": if guest.type == "qemu":
rows.append(("resume", "Arbeitsspeicher", "choice", RESUME_CHOICES)) rows.append(("resume", "Arbeitsspeicher", "choice", RESUME_CHOICES))
rows.append(("transfers", "Transfer-Laufwerke", "transfers", ())) 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 += [ rows += [
("newid", "Neue VMID", "text", ()), ("newid", "Neue VMID", "text", ()),
("node", "Node", "text", ()), ("node", "Node", "text", ()),
@@ -214,6 +222,10 @@ def _options(win, guest, spec):
"sie standen - ohne Crash-Recovery.", "sie standen - ohne Crash-Recovery.",
"transfers": "Vorbereitete Austauschmedien. Anlegen und befuellen " "transfers": "Vorbereitete Austauschmedien. Anlegen und befuellen "
"in der Uebersicht mit v .", "in der Uebersicht mit v .",
"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.", "newid": "VMID der neuen Maschine.",
"node": "Auf welchem Node sie laufen soll.", "node": "Auf welchem Node sie laufen soll.",
"start": "Sofort hochfahren.", "start": "Sofort hochfahren.",
@@ -321,6 +333,14 @@ def summary_lines(plan_):
% (human_bytes(plan_.vmstate_bytes) or "?") % (human_bytes(plan_.vmstate_bytes) or "?")
if plan_.resume else "vorhanden, wird aber nicht geladen"), if plan_.resume else "vorhanden, wird aber nicht geladen"),
curses.color_pair(C_OK) if plan_.resume else 0)) 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: for volume in plan_.transfers:
lines.append(("Transfer: %-14s %8s im Gast als %s" lines.append(("Transfer: %-14s %8s im Gast als %s"
% (volume.name, human_bytes(volume.size), volume.label), % (volume.name, human_bytes(volume.size), volume.label),
@@ -449,6 +469,9 @@ def _details(stdscr, proxmox, instance):
if instance.type == "lxc": if instance.type == "lxc":
lines.append((" oder auf dem Host: pct enter %d" % instance.vmid, lines.append((" oder auf dem Host: pct enter %d" % instance.vmid,
curses.color_pair(C_MARK))) 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: if instance.resumed:
lines.append(("", 0)) lines.append(("", 0))
@@ -713,6 +736,9 @@ def _spec_from_args(args, mode):
spec.net = args.net or "" spec.net = args.net or ""
spec.resume = args.resume spec.resume = args.resume
spec.transfers = list(args.transfer or []) 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.memory = args.memory or 0
spec.cores = args.cores or 0 spec.cores = args.cores or 0
spec.name = args.name or "" spec.name = args.name or ""
@@ -877,6 +903,28 @@ def cmd_cleanup(proxmox, args):
return 1 if failed else 0 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"
% (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): def cmd_console(proxmox, args):
instance = _find_instance(proxmox, args.vmid) instance = _find_instance(proxmox, args.vmid)
print(instance.console_url()) print(instance.console_url())
@@ -911,6 +959,14 @@ def _add_create_options(parser):
help="Arbeitsspeicher des Snapshots laden") help="Arbeitsspeicher des Snapshots laden")
parser.add_argument("--no-resume", dest="resume", action="store_false", parser.add_argument("--no-resume", dest="resume", action="store_false",
help="kalt starten, auch wenn der Snapshot RAM enthaelt") 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=[], parser.add_argument("--transfer", action="append", metavar="NAME", default=[],
help="vorbereitetes Transfer-Laufwerk anhaengen " help="vorbereitetes Transfer-Laufwerk anhaengen "
"(mehrfach moeglich)") "(mehrfach moeglich)")
@@ -978,6 +1034,12 @@ def build_parser():
sub = subs.add_parser(name, help=helptext) sub = subs.add_parser(name, help=helptext)
sub.add_argument("vmid", type=int) 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_ = subs.add_parser(
"flatten", help="vom Quell-Snapshot loesen - fuer den Dauerbetrieb") "flatten", help="vom Quell-Snapshot loesen - fuer den Dauerbetrieb")
flatten_.add_argument("vmid", type=int) flatten_.add_argument("vmid", type=int)
@@ -1020,6 +1082,7 @@ def main(argv=None):
"flatten": lambda: cmd_flatten(proxmox, args), "flatten": lambda: cmd_flatten(proxmox, args),
"cleanup": lambda: cmd_cleanup(proxmox, args), "cleanup": lambda: cmd_cleanup(proxmox, args),
"console": lambda: cmd_console(proxmox, args), "console": lambda: cmd_console(proxmox, args),
"spice": lambda: cmd_spice(proxmox, args),
"start": lambda: cmd_start(proxmox, args), "start": lambda: cmd_start(proxmox, args),
"stop": lambda: cmd_stop(proxmox, args), "stop": lambda: cmd_stop(proxmox, args),
} }
+102
View File
@@ -0,0 +1,102 @@
#!/bin/bash
# ---------------------------------------------------------------------
# Guest-Tools fuer wiederhergestellte Maschinen holen
#
# Die USB-Weiterleitung ueber SPICE braucht im Gast NICHTS - ein
# weitergereichter Dongle taucht dort als gewoehnliches USB-Geraet auf.
# Die Tools sind nur fuer den Komfort: Zwischenablage, automatische
# Aufloesung, sauberer Mauszeiger.
#
# Aufruf: ./get-guest-tools.sh [--storage <name>] [--list]
#
# --storage <name> wohin (Vorgabe: erster Storage mit iso-Inhalt)
# --list nur zeigen, was da ist
# -h, --help diese Hilfe
#
# Danach beim Anlegen einlegen:
# pvesnap-recovery live 802 --spice --usb 2 --iso <storage>:iso/<datei>
# ---------------------------------------------------------------------
set -euo pipefail
STORAGE=""
LIST=0
# virtio-win enthaelt fuer Windows alles auf einmal: den qxl-Grafiktreiber,
# den SPICE-Agenten und die virtio-Treiber. Fuer Linux gibt es nichts zu
# holen - dort genuegt das Paket der Distribution.
WINDOWS_URL="https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso"
WINDOWS_NAME="virtio-win.iso"
while [ $# -gt 0 ]; do
case "$1" in
--storage) STORAGE="${2:-}"; shift ;;
--storage=*) STORAGE="${1#*=}" ;;
--list) LIST=1 ;;
-h|--help) sed -n '2,19p' "$0" | sed 's/^#[[:space:]]\{0,1\}//'; exit 0 ;;
*) echo "Unbekannte Option: $1" >&2; exit 1 ;;
esac
shift
done
info() { printf '\033[1;32m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m==>\033[0m %s\n' "$*" >&2; }
fail() { printf '\033[1;31m==>\033[0m %s\n' "$*" >&2; exit 1; }
command -v pvesm >/dev/null 2>&1 || fail "pvesm nicht gefunden - das gehoert auf einen Proxmox-Host."
# Storage bestimmen: der erste, der iso-Inhalt aufnimmt
if [ -z "$STORAGE" ]; then
STORAGE="$(pvesm status --content iso 2>/dev/null | awk 'NR>1 && $3=="active" {print $1; exit}')"
[ -n "$STORAGE" ] || fail "Kein Storage mit iso-Inhalt gefunden.
In der Proxmox-Oberflaeche unter Datacenter -> Storage bei einem Storage
den Inhalt 'ISO image' zulassen, oder --storage angeben."
fi
DIR="$(pvesm path "${STORAGE}:iso/x" 2>/dev/null | xargs dirname 2>/dev/null || true)"
[ -n "$DIR" ] || fail "Ablageort von '${STORAGE}' nicht ermittelbar."
if [ "$LIST" -eq 1 ]; then
info "Vorhandene Abbilder auf '${STORAGE}' (${DIR}):"
pvesm list "$STORAGE" --content iso 2>/dev/null | tail -n +2 | sed 's/^/ /' \
|| echo " (keine)"
exit 0
fi
info "Ablage: ${DIR}"
mkdir -p "$DIR"
# --- Windows ---------------------------------------------------------
TARGET="${DIR}/${WINDOWS_NAME}"
if [ -f "$TARGET" ]; then
info "Schon da: ${STORAGE}:iso/${WINDOWS_NAME} ($(du -h "$TARGET" | cut -f1))"
else
info "Hole virtio-win (Windows: qxl-Treiber, SPICE-Agent, virtio-Treiber)"
command -v curl >/dev/null 2>&1 || fail "curl wird benoetigt."
if curl -fL --progress-bar -o "${TARGET}.teil" "$WINDOWS_URL"; then
mv "${TARGET}.teil" "$TARGET"
info "Fertig: ${STORAGE}:iso/${WINDOWS_NAME} ($(du -h "$TARGET" | cut -f1))"
else
rm -f "${TARGET}.teil"
fail "Herunterladen fehlgeschlagen. Hat der Host Internetzugang?
Sonst von Hand holen und nach ${DIR} legen:
${WINDOWS_URL}"
fi
fi
cat <<EOF
Beim Anlegen einlegen:
pvesnap-recovery live <VMID> --spice --usb 2 --iso ${STORAGE}:iso/${WINDOWS_NAME}
Im Gast danach:
Windows Von der CD 'virtio-win-guest-tools.exe' ausfuehren.
Linux Nichts von der CD - das Paket der Distribution nehmen:
apt install spice-vdagent (Debian/Ubuntu)
dnf install spice-vdagent (Fedora/RHEL)
Fuer einen weitergereichten USB-Dongle braucht es davon nichts. Der taucht im
Gast als gewoehnliches USB-Geraet auf, sobald du ihn im remote-viewer unter
"USB-Geraeteauswahl" anhakst.
EOF