summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Khirnov <anton@khirnov.net>2023-10-16 16:48:02 +0200
committerAnton Khirnov <anton@khirnov.net>2023-10-16 16:48:02 +0200
commit89014c64909f2c1c611a239ae28125c8483ae6e8 (patch)
tree6c8b7b4b4537a8018a04edbca4e0b39df52b32f6
parentfb0eab1bccf3dfd1b80ad5c3eacb9bde211a7440 (diff)
targets:TargetSSHLVM: implement backups from multiple volumes
-rw-r--r--lbup/_mountinfo.py6
-rw-r--r--lbup/targets.py108
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)