diff --git a/pvesnap/explorer.py b/pvesnap/explorer.py index 036bc78..a388f19 100644 --- a/pvesnap/explorer.py +++ b/pvesnap/explorer.py @@ -858,6 +858,11 @@ def _main(stdscr, args): try: _show_notes(stdscr, session.notes) 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] if len(mountpoints) > 1: entries = [(m, m) for m in mountpoints] diff --git a/pvesnap/snapfs.py b/pvesnap/snapfs.py index befcd0c..ffd7f0d 100644 --- a/pvesnap/snapfs.py +++ b/pvesnap/snapfs.py @@ -242,6 +242,8 @@ def cleanup_leftovers(verbose=False): run(["qemu-nbd", "--disconnect", value], check=False) elif kind == "lvm": run(["lvchange", "-an", value], check=False) + elif kind == "vgchange": + run(["vgchange", "-an", value], check=False) elif kind == "zfsclone": run(["zfs", "destroy", value], check=False) elif kind == "dir": @@ -303,9 +305,19 @@ class Session: except SnapfsError as exc: self.notes.append("%s: %s" % (volume.key, 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 " - "eingebunden werden.\n- " + "\n- ".join(self.notes)) + + if not self.mounts: + # 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 def _prepare_volume(self, volume): @@ -322,7 +334,17 @@ class Session: return 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) if device.mountable: try: @@ -519,6 +541,68 @@ class Session: walk(data.get("blockdevices") or []) 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): if device.path in self.mounts: return self.mounts[device.path] @@ -568,6 +652,8 @@ class Session: run(["qemu-nbd", "--disconnect", value], check=False) elif kind == "lvm": run(["lvchange", "-an", value], check=False) + elif kind == "vgchange": + run(["vgchange", "-an", value], check=False) elif kind == "zfsclone": run(["zfs", "destroy", value], check=False) elif kind == "dir" and os.path.isdir(value):