From 89014c64909f2c1c611a239ae28125c8483ae6e8 Mon Sep 17 00:00:00 2001 From: Anton Khirnov Date: Mon, 16 Oct 2023 16:48:02 +0200 Subject: targets:TargetSSHLVM: implement backups from multiple volumes --- lbup/_mountinfo.py | 6 --- lbup/targets.py | 108 +++++++++++++++++++++++++---------------------------- 2 files changed, 50 insertions(+), 64 deletions(-) diff --git a/lbup/_mountinfo.py b/lbup/_mountinfo.py index d2640ce..dc925d8 100644 --- a/lbup/_mountinfo.py +++ b/lbup/_mountinfo.py @@ -137,12 +137,6 @@ class MountInfo: def __str__(self): return '%s(%d entries)' % (self.__class__.__name__, len(self.mounts)) - def entries_for_mountpoint(self, mountpoint): - """ - Iterate over all mountinfo entries mounted at the given mountpoint. - """ - return filter(lambda entry: entry.mount_point == mountpoint, self.mounts.values()) - def mountpoint_for_path(self, path): """ Find the mount entry which contains path. diff --git a/lbup/targets.py b/lbup/targets.py index 3f86563..3473ed1 100644 --- a/lbup/targets.py +++ b/lbup/targets.py @@ -35,43 +35,19 @@ def _parse_name(name): return SSHRemote(host, port, username) -def _resolve_mntdev(mntinfo, dirs): - """ - Find the device on which dirs to back up are located. - - This also checks that all the dirs are on the same mount and no non-trivial - topologies (like bind mounts) are involved, - otherwise a BackupException is raised. - - Return a tuple of (devnum, mountpoint, fstype) - """ - devnum = None - mountpoint = None - fstype = None - for d in dirs: - d = AbsPath(d) - - mp = mntinfo.mountpoint_for_path(d) - e = list(mntinfo.entries_for_mountpoint(mp)) - - if len(e) != 1: - raise BackupException('Expected exactly one mountpoint for dir', d, str(e)) - if e[0].root != ROOT: - raise BackupException('Mountpoint is a bind mount, which is not supported', str(e[0])) - dn = e[0].devnum - - if devnum is None: - devnum = dn - mountpoint = mp - fstype = e[0].fstype - continue - - if dn != devnum or mp != mountpoint: - raise BackupException('Mismatching device numbers/mountpoints', - dn, devnum, mp, mountpoint) +def _mounts_for_dirs(mntinfo, dirs): + mounts = list(sorted(set(mntinfo.mountpoint_for_path(d) for d in dirs), + key = lambda m: m.index)) + if len(mounts) == 0: + raise RuntimeError('Expected at least one mount, got zero') - return (devnum, mountpoint, fstype) + for i, m in enumerate(mounts): + if m.root != ROOT: + raise ValueError('Offset root in mount, this is not supported', m) + if i and m.parent not in mounts[:i]: + raise ValueError('Mount has an intermediate parent, this is not supported', m) + return mounts class Target(ABC): name = None @@ -238,18 +214,20 @@ class TargetSSHLVM(TargetSSH): """ This target backs up a remote host using LVM snapshots. - All the dirs backed up must be on same LV. Requires root login on the system. + + :param str path_prefix: Prefix to be added to all paths in dirs and + excludes. This prefix will not appear in the backups. """ _snapshot_size = None - _strip_mountpoint = None + _path_prefix = None def __init__(self, name, dirs, excludes = None, logger = None, remote = None, remote_bupdir = None, snapshot_size = '20G', - strip_mountpoint = False): + path_prefix = '/'): self._snapshot_size = snapshot_size - self._strip_mountpoint = strip_mountpoint + self._path_prefix = AbsPath(path_prefix) super().__init__(name, dirs, excludes, logger, remote, remote_bupdir) @@ -335,44 +313,58 @@ class TargetSSHLVM(TargetSSH): conn_root = stack.enter_context(_ssh_client.SSHConnection(root_remote)) # resolve the remote paths - bupdir = self._resolve_remote_path(conn_tgt, self._remote_bupdir) - dirs = [self._resolve_remote_path(conn_tgt, d) for d in self.dirs] - excludes = [self._resolve_remote_path(conn_tgt, d) for d in self.excludes] + bupdir = self._resolve_remote_path(conn_tgt, self._remote_bupdir) + dirs = [self._resolve_remote_path(conn_tgt, self._path_prefix + d) for d in self.dirs] + excludes = [self._resolve_remote_path(conn_tgt, self._path_prefix + d) for d in self.excludes] # make sure the mount directory exists # due to how bup index works, the mount directory has to stay the # same for each backup # we use BUP_DIR/lbup_mount - snapshot_mount = '%s/%s' % (bupdir, 'lbup_mount') - self._paramiko_exec_cmd(conn_tgt, 'mkdir -p -m 700 ' + snapshot_mount) + snapshot_root = bupdir + 'lbup_mount' + self._paramiko_exec_cmd(conn_tgt, 'mkdir -p -m 700 ' + str(snapshot_root)) # read and parse the mountinfo table mntinfo = MountInfo( self._paramiko_exec_cmd(conn_root, 'cat /proc/1/mountinfo', decode = False)) - devnum, mountpoint, fstype = _resolve_mntdev(mntinfo, dirs) + mounts = _mounts_for_dirs(mntinfo, dirs) + + for mnt in mounts: + self._logger.debug('Processing mount: %s', str(mnt)) + + # make sure we have a valid fstype + fstype = mnt.fstype.decode('ascii') + if not re.fullmatch(r'\w+', fstype, re.ASCII): + raise BackupException('Invalid LV FS type', fstype) + + # The volumes to snapshot are sorted by mount order. + # We reparent the snapshot mountpoints such that the topmost one + # ends up at snapshot_root, and the rest (if any) have the same + # relative position to the top one as they do in the real + # hierarchy + snapshot_path = mnt.mount_point.reparent(mounts[0].mount_point, snapshot_root) - # make sure we have a valid fstype - fstype = fstype.decode('ascii') - if not re.fullmatch(r'\w+', fstype, re.ASCII): - raise BackupException('Invalid LV FS type', fstype) + self._logger.debug('Snapshotting device %s(%s) mounted at %s', + "%d:%d" % (mnt.devnum >> 8, mnt.devnum & 255), fstype, + str(mnt.mount_point)) - self._logger.debug('Backup targets are at device %s(%s), mounted at %s', - "%d:%d" % (devnum >> 8, devnum & 255), fstype, mountpoint) + stack.enter_context(self._mount_snapshot(conn_root, mnt.devnum, + str(snapshot_path), fstype)) bup_exec = ['bup', 'on', '%s@%s' % (self._remote.username, self._remote.host), '-d', str(bupdir)] - index_path = '%s/%s.index' % (bupdir, self.name) - save_opts = ['--graft=%s=%s' % (snapshot_mount, '/' if self._strip_mountpoint else mountpoint), - '--indexfile=%s' % index_path] - index_opts = ['--no-check-device', '--indexfile=%s' % index_path] - reparent = (mountpoint, AbsPath(snapshot_mount)) + mnt_offset = mounts[0].mount_point.reparent(self._path_prefix, ROOT) + + index_path = bupdir + self.name + save_opts = ['--graft=%s=%s' % (snapshot_root, mnt_offset), '--indexfile=%s' % str(index_path)] + index_opts = ['--no-check-device', '--indexfile=%s' % str(index_path)] + + reparent = (mounts[0].mount_point, snapshot_root) dirs = [str(d.reparent(*reparent)) for d in dirs] excludes = [str(d.reparent(*reparent)) for d in excludes] - stack.enter_context(self._mount_snapshot(conn_root, devnum, snapshot_mount, fstype)) - return self._do_save(bup_exec, dry_run, dirs = dirs, excludes = excludes, save_opts = save_opts, index_opts = index_opts) -- cgit v1.2.3