Transfer-Laufwerke im laufenden Betrieb wechseln

Auswerfen und wieder einklinken, ohne die Maschine anzufassen - damit laesst
sich mehrfach nachladen und abholen, statt nur einmal am Ende.

    eject_transfer()    zieht die Platte ab, danach gehoert sie dem Host
    insert_transfer()   gibt sie zurueck in denselben Steckplatz
    transfer_state()    was gerade wirklich steckt

In der Detailansicht auf Taste e; bei mehreren Laufwerken mit Auswahl. Vor dem
Auswerfen wird nachgefragt, mit dem Hinweis, im Gast vorher auszuhaengen -
Proxmox meldet das Geraet zwar ab, aber ein Dateisystem, das noch beschrieben
wird, nimmt das uebel. Schlaegt es fehl, weil der Gast es festhaelt, steht das
auch so in der Meldung.

Beim Container gibt es nichts zu wechseln und das wird auch so gesagt: dort
ist das Verzeichnis durchgereicht, Host und Gast sehen dieselben Dateien
ohnehin gleichzeitig.

Dabei ein Fehler aufgefallen, den erst der Test zeigte: bei einer Maschine mit
geladenem Arbeitsspeicher steckte das Transfer-Laufwerk in der kalten
Konfiguration - der Gast haette es nie bemerkt. Dieselbe Falle wie beim
Austauschlaufwerk, dort schon behoben, hier vergessen. Beide gehen jetzt durch
dieselbe Nachsteck-Logik.

Am laufenden System durchgespielt, mit RAM-Zustand:

    Start        "Stecke Transfer-Laufwerk 'hotswap' als scsi2 an"
    auswerfen    eingesteckt=False, Laufwerk wieder frei
    befuellen    runde2.txt vom Host, VM lief weiter
    einklinken   scsi2: /dev/loop0,backup=0,size=1G
    Ende         beide Dateien da, VM durchgehend running

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
duffyduck
2026-08-03 09:24:47 +02:00
co-authored by Claude Opus 5
parent 2b895e6f4f
commit 0a92ec0984
2 changed files with 193 additions and 14 deletions
+141 -12
View File
@@ -955,16 +955,25 @@ def create(proxmox, plan_, progress=None):
if guest.type == "lxc": if guest.type == "lxc":
# Beim Container wird der Mountpunkt des Hosts durchgereicht. # Beim Container wird der Mountpunkt des Hosts durchgereicht.
# Ein Bind-Mount ist dasselbe Dateisystem an zwei Stellen, kein # Ein Bind-Mount ist dasselbe Dateisystem an zwei Stellen, kein
# zweites Einhaengen - das ist unbedenklich. # zweites Einhaengen - Host und Gast koennen beide arbeiten.
step("Reiche Transfer-Laufwerk %r durch" % volume.name) step("Reiche Transfer-Laufwerk %r durch" % volume.name)
path = transfer.host_mount(volume.name) path = transfer.host_mount(volume.name)
slot = _free_mp_slot(new_config) slot = _free_mp_slot(new_config)
new_config[slot] = "%s,mp=/mnt/%s" % (path, volume.name) value = "%s,mp=/mnt/%s" % (path, volume.name)
new_config[slot] = value
attached.append({"name": volume.name, "key": slot, "drive": value,
"kind": "bind", "pending": False})
else: else:
step("Haenge Transfer-Laufwerk %r an" % volume.name) step("Haenge Transfer-Laufwerk %r an" % volume.name)
slot = _free_disk_slot(new_config) slot = _free_disk_slot(new_config)
new_config[slot] = transfer.drive_string(volume.name) value = transfer.drive_string(volume.name)
attached.append(volume.name) # Mit geladenem Arbeitsspeicher wacht der Gast in einem Zustand
# auf, in dem es die Platte nicht gab - dann muss sie erst nach
# dem Fortsetzen dazukommen, sonst bemerkt er sie nie.
if not plan_.resume:
new_config[slot] = value
attached.append({"name": volume.name, "key": slot, "drive": value,
"kind": "disk", "pending": bool(plan_.resume)})
# -- restliche Anpassungen ----------------------------------------- # -- restliche Anpassungen -----------------------------------------
if spec.memory: if spec.memory:
@@ -1071,10 +1080,32 @@ def start(proxmox, instance, progress=None):
def _attach_pending(proxmox, instance, step=None): def _attach_pending(proxmox, instance, step=None):
"""Das Austauschlaufwerk an die schon laufende Maschine anstecken.""" """Was erst nach dem Fortsetzen dazukommen darf, jetzt anstecken."""
notes = []
for entry in _transfer_entries(instance):
if not entry.get("pending"):
continue
if step:
step("Stecke Transfer-Laufwerk %r als %s an"
% (entry["name"], entry["key"]))
try:
proxmox.set_guest_config(instance.guest, {entry["key"]: entry["drive"]})
config = proxmox.guest_config(instance.guest)
except ProxmoxError as exc:
notes.append("Transfer-Laufwerk %r liess sich nicht anstecken: %s"
% (entry["name"], exc))
continue
if entry["key"] not in config:
notes.append("Transfer-Laufwerk %r ist eingetragen, aber nicht "
"angesteckt - es erscheint erst nach einem Neustart."
% entry["name"])
else:
entry["pending"] = False
_registry_add(instance)
exchange = instance.exchange or {} exchange = instance.exchange or {}
if not exchange.get("pending"): if not exchange.get("pending"):
return "" return " ".join(notes)
key, drive = exchange.get("key"), exchange.get("drive") key, drive = exchange.get("key"), exchange.get("drive")
if not key or not drive: if not key or not drive:
return "" return ""
@@ -1084,16 +1115,114 @@ def _attach_pending(proxmox, instance, step=None):
proxmox.set_guest_config(instance.guest, {key: drive}) proxmox.set_guest_config(instance.guest, {key: drive})
config = proxmox.guest_config(instance.guest) config = proxmox.guest_config(instance.guest)
except ProxmoxError as exc: except ProxmoxError as exc:
return "Das Austauschlaufwerk liess sich nicht anstecken: %s" % exc notes.append("Das Austauschlaufwerk liess sich nicht anstecken: %s" % exc)
return " ".join(notes)
if key not in config: if key not in config:
# Proxmox hat es nur vorgemerkt - im Gast taucht es dann nicht auf. # Proxmox hat es nur vorgemerkt - im Gast taucht es dann nicht auf.
return ("Das Austauschlaufwerk ist eingetragen, aber nicht angesteckt " notes.append("Das Austauschlaufwerk ist eingetragen, aber nicht "
"worden (Hotplug fuer Platten ist bei dieser Maschine aus). " "angesteckt worden (Hotplug fuer Platten ist bei dieser "
"Es erscheint erst nach einem Neustart des Gastes.") "Maschine aus). Es erscheint erst nach einem Neustart.")
else:
exchange["pending"] = False exchange["pending"] = False
_registry_add(instance) _registry_add(instance)
return "" return " ".join(notes)
def _transfer_entries(instance):
"""Die Transfer-Laufwerke einer Instanz als Datensaetze.
Aeltere Merklisten enthalten nur Namen - die werden hier mitgelesen.
"""
entries = []
for entry in instance.transfers or []:
if isinstance(entry, dict):
entries.append(entry)
else:
entries.append({"name": str(entry), "key": "", "drive": "",
"kind": "disk", "pending": False})
return entries
def transfer_state(proxmox, instance):
"""[(Datensatz, eingesteckt?)] - was gerade in der Maschine steckt."""
try:
config = proxmox.guest_config(instance.guest)
except ProxmoxError:
config = {}
return [(entry, bool(entry.get("key") and entry["key"] in config))
for entry in _transfer_entries(instance)]
def eject_transfer(proxmox, instance, name, progress=None):
"""Ein Transfer-Laufwerk bei laufender Maschine abziehen.
Danach gehoert es wieder dem Host: einhaengen, befuellen, auslesen - und
mit insert_transfer() wieder hineingeben, ohne die Maschine anzufassen.
Wichtig: im Gast vorher aushaengen. Proxmox meldet das Geraet ab, aber ein
Dateisystem, das noch beschrieben wird, nimmt das uebel.
"""
def step(text):
log.info("%s", text)
if progress:
progress(text)
entry = next((e for e in _transfer_entries(instance) if e["name"] == name), None)
if entry is None:
raise RecoveryError("%r haengt nicht an %s." % (name, instance.label))
if entry.get("kind") == "bind":
raise RecoveryError(
"Bei Containern gibt es nichts auszuwerfen: das Verzeichnis ist "
"durchgereicht, Host und Gast sehen dieselben Dateien sofort.")
if not entry.get("key"):
raise RecoveryError("Zu %r ist kein Steckplatz vermerkt." % name)
step("Werfe %r aus (%s)" % (name, entry["key"]))
try:
proxmox.set_guest_config(instance.guest, {"delete": entry["key"]})
except ProxmoxError as exc:
raise RecoveryError(
"%r liess sich nicht abziehen: %s\n"
"Meist haelt der Gast es noch - dort erst aushaengen "
"(Linux: umount, Windows: Auswerfen im Explorer)." % (name, exc))
entry["pending"] = True
_registry_add(instance)
transfer.release(name)
step("%r gehoert jetzt wieder dem Host" % name)
return True
def insert_transfer(proxmox, instance, name, progress=None):
"""Ein zuvor ausgeworfenes Transfer-Laufwerk wieder einklinken."""
def step(text):
log.info("%s", text)
if progress:
progress(text)
entry = next((e for e in _transfer_entries(instance) if e["name"] == name), None)
if entry is None:
raise RecoveryError("%r gehoert nicht zu %s." % (name, instance.label))
if entry.get("kind") == "bind":
raise RecoveryError("Bei Containern ist das Verzeichnis dauerhaft "
"durchgereicht - nichts einzuklinken.")
volume = transfer.get(name)
if volume.mountpoint:
step("%r ist noch auf dem Host eingehaengt - haenge aus" % name)
transfer.host_umount(name)
drive = transfer.drive_string(name)
entry["drive"] = drive
step("Klinke %r wieder ein (%s)" % (name, entry["key"]))
try:
proxmox.set_guest_config(instance.guest, {entry["key"]: drive})
except ProxmoxError as exc:
raise RecoveryError("%r liess sich nicht einklinken: %s" % (name, exc))
entry["pending"] = False
_registry_add(instance)
return True
def _default_name(guest, plan_): def _default_name(guest, plan_):
@@ -1495,7 +1624,7 @@ def destroy(proxmox, instance, progress=None, keep_snapshot_protection=False):
except RecoveryError as exc: except RecoveryError as exc:
log.warning("Schutz von %s@%s blieb: %s", volid, snapname, exc) log.warning("Schutz von %s@%s blieb: %s", volid, snapname, exc)
for name in instance.transfers or []: for name in [e["name"] for e in _transfer_entries(instance)]:
step("Gebe Transfer-Laufwerk %r wieder frei" % name) step("Gebe Transfer-Laufwerk %r wieder frei" % name)
try: try:
transfer.host_umount(name) transfer.host_umount(name)
+51 -1
View File
@@ -27,6 +27,7 @@ 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 (EXCHANGE_GUEST_PATH, RecoveryError, Spec, create, from .recovery import (EXCHANGE_GUEST_PATH, RecoveryError, Spec, create,
eject_transfer, insert_transfer, transfer_state,
destroy, find_orphans, flatten, flatten_cost, flatten_hint, destroy, find_orphans, flatten, flatten_cost, flatten_hint,
human_bytes, human_bytes,
linked_volumes, list_instances, plan, pull, release, linked_volumes, list_instances, plan, pull, release,
@@ -476,6 +477,24 @@ def _details(stdscr, proxmox, instance):
"ersten Start lief die Maschine genau weiter.", "ersten Start lief die Maschine genau weiter.",
curses.color_pair(C_DIM))) 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: try:
linked = linked_volumes(proxmox, instance) linked = linked_volumes(proxmox, instance)
except (RecoveryError, ProxmoxError): except (RecoveryError, ProxmoxError):
@@ -493,7 +512,7 @@ def _details(stdscr, proxmox, instance):
lines.append(("Datentraeger: eigenstaendig - haengt an keinem Snapshot", lines.append(("Datentraeger: eigenstaendig - haengt an keinem Snapshot",
curses.color_pair(C_OK))) curses.color_pair(C_OK)))
footer = ("s Starten | h Herunterfahren | a Austausch | f Loesen | " footer = ("s Start | h Stop | e Auswerfen | a Austausch | f Loesen | "
"x Verwerfen | q zurueck") "x Verwerfen | q zurueck")
key = _pages(stdscr, lines, "Wiederherstellung %s" % instance.label, footer) key = _pages(stdscr, lines, "Wiederherstellung %s" % instance.label, footer)
@@ -507,6 +526,8 @@ def _details(stdscr, proxmox, instance):
lambda: proxmox.shutdown_guest(instance.guest)) lambda: proxmox.shutdown_guest(instance.guest))
elif key in ("a", "A"): elif key in ("a", "A"):
_pull(stdscr, proxmox, instance) _pull(stdscr, proxmox, instance)
elif key in ("e", "E"):
_swap(stdscr, proxmox, instance, steckplaetze)
elif key in ("f", "F"): elif key in ("f", "F"):
_flatten(stdscr, proxmox, instance, linked) _flatten(stdscr, proxmox, instance, linked)
elif key in ("x", "X"): elif key in ("x", "X"):
@@ -545,6 +566,35 @@ def _pull(stdscr, proxmox, instance):
]), "Austauschlaufwerk") ]), "Austauschlaufwerk")
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.", 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): def _flatten(stdscr, proxmox, instance, linked):
if not linked: if not linked:
message(stdscr, "%s haengt an keinem Snapshot mehr." % instance.label) message(stdscr, "%s haengt an keinem Snapshot mehr." % instance.label)