Explorer: Absturz beheben und LVM im Gast unterstuetzen

Beim Einbinden stuerzte der Explorer mit IndexError ab. Ursache: prepare()
meldete nur dann einen Fehler, wenn gar kein Geraet gefunden wurde - nicht
aber, wenn zwar Geraete da waren, sich davon aber keins einhaengen liess.
Der Aufrufer griff dann blind auf mountpoints[0] zu.

* prepare() meldet jetzt einen Fehler, sobald kein Dateisystem eingehaengt
  werden konnte, und nennt dabei die gefundenen Geraete samt Dateisystem-
  Typ - sonst raet man beim Suchen nur.
* Der Explorer prueft die Liste zusaetzlich selbst ab.

Der wahrscheinlichste Grund fuer "gefunden, aber nicht mountbar" ist LVM
im Gast: dahinter liegt zunaechst nur ein LVM2_member. Das wird jetzt
unterstuetzt - inklusive der heiklen Stelle: heisst die Volume-Group im
Gast genauso wie eine auf dem Host (typisch 'pve'), waere nicht mehr
eindeutig, welche gemeint ist. Erkannt wird das an gleichem Namen bei
unterschiedlicher UUID; dann wird bewusst nichts aktiviert und der Grund
angezeigt. Beim Aufraeumen wird die Gruppe vor dem Aushaengen des Geraets
wieder deaktiviert.

Ausserdem "udevadm settle" nach dem Einbinden - die Partitionsgeraete
tauchen sonst manchmal erst nach dem Scan auf.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
duffyduck
2026-07-31 09:35:22 +02:00
co-authored by Claude Opus 5
parent f37a942fea
commit da08a81aab
2 changed files with 95 additions and 4 deletions
+5
View File
@@ -858,6 +858,11 @@ def _main(stdscr, args):
try: try:
_show_notes(stdscr, session.notes) _show_notes(stdscr, session.notes)
mountpoints = sorted(set(session.mounts.values())) mountpoints = sorted(set(session.mounts.values()))
if not mountpoints:
_show_notes(stdscr,
["Es wurde kein Dateisystem eingehaengt."] + session.notes,
"Nichts zu zeigen")
return 3
start = mountpoints[0] start = mountpoints[0]
if len(mountpoints) > 1: if len(mountpoints) > 1:
entries = [(m, m) for m in mountpoints] entries = [(m, m) for m in mountpoints]
+90 -4
View File
@@ -242,6 +242,8 @@ def cleanup_leftovers(verbose=False):
run(["qemu-nbd", "--disconnect", value], check=False) run(["qemu-nbd", "--disconnect", value], check=False)
elif kind == "lvm": elif kind == "lvm":
run(["lvchange", "-an", value], check=False) run(["lvchange", "-an", value], check=False)
elif kind == "vgchange":
run(["vgchange", "-an", value], check=False)
elif kind == "zfsclone": elif kind == "zfsclone":
run(["zfs", "destroy", value], check=False) run(["zfs", "destroy", value], check=False)
elif kind == "dir": elif kind == "dir":
@@ -303,9 +305,19 @@ class Session:
except SnapfsError as exc: except SnapfsError as exc:
self.notes.append("%s: %s" % (volume.key, exc)) self.notes.append("%s: %s" % (volume.key, exc))
log.warning("%s (%s): %s", volume.key, volume.volid, exc) log.warning("%s (%s): %s", volume.key, volume.volid, exc)
if not self.mounts and not self.devices:
raise SnapfsError("Kein Datentraeger des Snapshots konnte " if not self.mounts:
"eingebunden werden.\n- " + "\n- ".join(self.notes)) # Ohne Mountpunkt gibt es nichts zu durchsuchen. Was gefunden wurde,
# gehoert trotzdem in die Meldung - sonst raet man nur.
details = list(self.notes)
if self.devices:
details.append("Gefunden wurden: %s"
% ", ".join("%s (%s, %s)"
% (d.path, d.fstype or "kein Dateisystem",
d.human_size)
for d in self.devices))
raise SnapfsError("Kein Dateisystem des Snapshots liess sich einhaengen."
+ ("\n- " + "\n- ".join(details) if details else ""))
return self.mounts return self.mounts
def _prepare_volume(self, volume): def _prepare_volume(self, volume):
@@ -322,7 +334,17 @@ class Session:
return return
device_path = self._map(info, volume) device_path = self._map(info, volume)
for device in self._scan(device_path, volume): run(["udevadm", "settle", "--timeout=10"], check=False)
devices = self._scan(device_path, volume)
# Viele Gaeste legen ihre Dateisysteme in LVM ab - dann liegt hinter der
# Partition erst einmal nur ein "LVM2_member".
for device in list(devices):
if device.fstype == "LVM2_member":
devices.extend(self._activate_guest_lvm(device, volume))
for device in devices:
self.devices.append(device) self.devices.append(device)
if device.mountable: if device.mountable:
try: try:
@@ -519,6 +541,68 @@ class Session:
walk(data.get("blockdevices") or []) walk(data.get("blockdevices") or [])
return found return found
# -- LVM im Gast ------------------------------------------------------
def _activate_guest_lvm(self, device, volume):
"""Volume-Group des Gastes aktivieren und ihre Volumes zurueckgeben.
Heikel ist der Namenskonflikt: heisst die Gruppe im Gast genauso wie
eine auf dem Host (typisch 'pve'), waere nicht mehr eindeutig, welche
gemeint ist. In dem Fall wird bewusst nichts aktiviert.
"""
if not have("pvs") or not have("vgchange"):
self.notes.append("%s enthaelt LVM, aber die LVM-Werkzeuge fehlen"
% device.path)
return []
groups = self._physical_volumes()
mine = groups.get(os.path.realpath(device.path)) or groups.get(device.path)
if not mine:
self.notes.append("%s: keine Volume-Group gefunden" % device.path)
return []
name, uuid = mine
# Gleicher Name, andere UUID = Konflikt mit einer Gruppe des Hosts.
for other_path, (other_name, other_uuid) in groups.items():
if other_name == name and other_uuid != uuid:
self.notes.append(
"LVM-Gruppe '%s' aus dem Gast heisst genauso wie eine auf dem "
"Host (%s) - sie wird nicht aktiviert, weil sonst nicht "
"eindeutig waere, welche gemeint ist." % (name, other_path))
return []
try:
run(["vgchange", "-ay", "--readonly", name], timeout=60)
except SnapfsError as exc:
self.notes.append("Volume-Group '%s' nicht aktivierbar: %s" % (name, exc))
return []
self._remember("vgchange", name)
run(["udevadm", "settle", "--timeout=10"], check=False)
log.info("LVM-Gruppe '%s' des Gastes aktiviert", name)
# Nach dem Aktivieren haengen die Logical Volumes unter dem Geraet.
found = [d for d in self._lsblk(device.path, volume)
if d.path != device.path and d.kind == "lvm"]
if not found:
self.notes.append("Volume-Group '%s' enthaelt keine lesbaren Volumes" % name)
return found
@staticmethod
def _physical_volumes():
"""{PV-Pfad: (VG-Name, VG-UUID)} - auch fuer noch nicht aktive Gruppen."""
try:
output = run(["pvs", "--noheadings", "--nosuffix", "-o",
"pv_name,vg_name,vg_uuid", "--separator", "|"], timeout=60)
except SnapfsError:
return {}
result = {}
for line in output.splitlines():
parts = [p.strip() for p in line.strip().split("|")]
if len(parts) >= 3 and parts[0] and parts[1]:
result[parts[0]] = (parts[1], parts[2])
return result
def mount(self, device): def mount(self, device):
if device.path in self.mounts: if device.path in self.mounts:
return self.mounts[device.path] return self.mounts[device.path]
@@ -568,6 +652,8 @@ class Session:
run(["qemu-nbd", "--disconnect", value], check=False) run(["qemu-nbd", "--disconnect", value], check=False)
elif kind == "lvm": elif kind == "lvm":
run(["lvchange", "-an", value], check=False) run(["lvchange", "-an", value], check=False)
elif kind == "vgchange":
run(["vgchange", "-an", value], check=False)
elif kind == "zfsclone": elif kind == "zfsclone":
run(["zfs", "destroy", value], check=False) run(["zfs", "destroy", value], check=False)
elif kind == "dir" and os.path.isdir(value): elif kind == "dir" and os.path.isdir(value):