Transfer-Laufwerke: nur noch exFAT, Oberflaeche und Anbindung

Ein einziges Dateisystem, mit Absicht: exFAT koennen Windows und Linux beide
von Haus aus, es kennt keine Besitzrechte auf dem Datentraeger und hat anders
als FAT32 keine 4-GB-Grenze je Datei. Die Auswahl faellt damit weg - eine
Entscheidung weniger, wenn es schnell gehen muss.

Neu in der Oberflaeche:

  Taste v          Uebersicht der Laufwerke mit Zustand
  Enter darauf     haengt am Host ein und startet den Commander - links das
                   Laufwerk, rechts der eigene Rechner
  n / u / l        anlegen, aushaengen, loeschen
  Optionsmaske     Mehrfachauswahl statt Groessenangabe
  --transfer NAME  dasselbe auf der Kommandozeile, mehrfach moeglich

Am laufenden System durchgespielt: Laufwerk angelegt, vom Host ein Skript
hineingelegt, Wiederherstellung damit gestartet, im Gast gelesen, ausgefuehrt
und geschrieben, Wiederherstellung verworfen - das Laufwerk lebte samt
Ergebnis weiter.

Zwei Rechtefallen dabei gefunden und geschlossen:

  Der Gast konnte lesen, aber nicht schreiben. exFAT hat keine Besitzrechte
  auf der Platte, die entstehen beim Einhaengen - mit der Vorgabe gehoert
  alles root, und der root eines unprivilegierten Containers ist auf dem Host
  uid 100000. Wird jetzt mit umask=0000 eingehaengt.

  Ein durchgereichtes Laufwerk galt als "am Host eingehaengt", weil beim
  Container genau das der Fall ist. Wer das sah, haette es aushaengen und dem
  laufenden Gast den Boden wegziehen koennen. Die Belegung wird jetzt auch aus
  Container-Konfigurationen gelesen, der Gast hat Vorrang in der Anzeige, und
  Aushaengen wird abgelehnt, solange jemand darauf arbeitet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
duffyduck
2026-07-31 22:12:18 +02:00
co-authored by Claude Opus 5
parent 6279a20d4a
commit 2b895e6f4f
4 changed files with 428 additions and 98 deletions
+49 -4
View File
@@ -35,6 +35,7 @@ import subprocess
import time
from dataclasses import dataclass, field
from . import transfer
from .proxmox import Guest, ProxmoxError
log = logging.getLogger("pvesnap.recovery")
@@ -477,6 +478,7 @@ class Spec:
exchange_storage: str = ""
exchange_fs: str = DEFAULT_EXCHANGE_FS
exchange_dir: str = "" # nur LXC: vorhandenes Host-Verzeichnis
transfers: list = field(default_factory=list) # Namen vorhandener Laufwerke
memory: int = 0 # 0 = wie im Snapshot
cores: int = 0
name: str = ""
@@ -504,6 +506,7 @@ class Plan:
resume: bool = False
exchange_kind: str = "" # "disk" | "bind" | ""
exchange_detail: str = ""
transfers: list = field(default_factory=list) # [transfer.Volume]
warnings: list = field(default_factory=list)
# Was nicht nur unschoen, sondern gefaehrlich ist. Hierfuer genuegt ein
# beilaeufiges "ja" nicht - siehe cmd_create() und die Oberflaeche.
@@ -531,6 +534,7 @@ class Instance:
volumes: list = field(default_factory=list)
protected: list = field(default_factory=list) # [[volid, snapname]]
exchange: dict = field(default_factory=dict)
transfers: list = field(default_factory=list)
resumed: bool = False
status: str = ""
@@ -555,14 +559,15 @@ class Instance:
def to_dict(self):
return {k: getattr(self, k) for k in
("vmid", "type", "node", "name", "source", "source_node", "snapshot",
"mode", "created", "volumes", "protected", "exchange", "resumed")}
"mode", "created", "volumes", "protected", "exchange", "transfers",
"resumed")}
@classmethod
def from_dict(cls, data):
known = {k: data.get(k) for k in
("vmid", "type", "node", "name", "source", "source_node", "snapshot",
"mode", "created", "volumes", "protected", "exchange", "resumed")
if data.get(k) is not None}
"mode", "created", "volumes", "protected", "exchange", "transfers",
"resumed") if data.get(k) is not None}
known["vmid"] = int(known.get("vmid") or 0)
return cls(**known)
@@ -796,6 +801,14 @@ def plan(proxmox, guest, snapname, spec):
result.exchange_detail = "%s auf %s, %s, im Gast als weitere Platte" % (
human_bytes(size_kb * 1024), storage, spec.exchange_fs)
# -- Transfer-Laufwerke ------------------------------------------------
for name in spec.transfers:
volume = transfer.get(name) # wirft, wenn es das nicht gibt
if not volume.free:
raise RecoveryError("Transfer-Laufwerk %r ist gerade %s."
% (name, volume.state_text))
result.transfers.append(volume)
# -- Hinweise ----------------------------------------------------------
if result.dropped and not spec.keep_binds:
result.notes.append("Nicht uebernommen werden Einbindungen von Host-"
@@ -936,6 +949,23 @@ def create(proxmox, plan_, progress=None):
exchange = {"kind": "bind", "path": host_dir, "key": slot,
"own": not spec.exchange_dir}
# -- Transfer-Laufwerke --------------------------------------------
attached = []
for volume in plan_.transfers:
if guest.type == "lxc":
# Beim Container wird der Mountpunkt des Hosts durchgereicht.
# Ein Bind-Mount ist dasselbe Dateisystem an zwei Stellen, kein
# zweites Einhaengen - das ist unbedenklich.
step("Reiche Transfer-Laufwerk %r durch" % volume.name)
path = transfer.host_mount(volume.name)
slot = _free_mp_slot(new_config)
new_config[slot] = "%s,mp=/mnt/%s" % (path, volume.name)
else:
step("Haenge Transfer-Laufwerk %r an" % volume.name)
slot = _free_disk_slot(new_config)
new_config[slot] = transfer.drive_string(volume.name)
attached.append(volume.name)
# -- restliche Anpassungen -----------------------------------------
if spec.memory:
new_config["memory"] = spec.memory
@@ -957,7 +987,8 @@ def create(proxmox, plan_, progress=None):
name=str(new_config.get("name") or new_config.get("hostname") or ""),
source=guest.vmid, source_node=guest.node, snapshot=plan_.snapshot,
mode=spec.mode, created=int(time.time()), volumes=created,
protected=protected, exchange=exchange, resumed=plan_.resume)
protected=protected, exchange=exchange, resumed=plan_.resume,
transfers=attached)
_registry_add(instance)
return instance
@@ -980,6 +1011,12 @@ def create(proxmox, plan_, progress=None):
pass
for path in made_dirs:
shutil.rmtree(path, ignore_errors=True)
for volume in plan_.transfers:
try:
transfer.host_umount(volume.name)
transfer.release(volume.name)
except transfer.TransferError:
pass
raise
@@ -1458,6 +1495,14 @@ def destroy(proxmox, instance, progress=None, keep_snapshot_protection=False):
except RecoveryError as exc:
log.warning("Schutz von %s@%s blieb: %s", volid, snapname, exc)
for name in instance.transfers or []:
step("Gebe Transfer-Laufwerk %r wieder frei" % name)
try:
transfer.host_umount(name)
transfer.release(name)
except transfer.TransferError as exc:
log.warning("%s: %s", name, exc)
exchange = instance.exchange or {}
if exchange.get("kind") == "bind" and exchange.get("own") and exchange.get("path"):
step("Entferne Austauschverzeichnis %s" % exchange["path"])