From 21c759115774e7ab980b210d6f806938fa0b12e5 Mon Sep 17 00:00:00 2001 From: Anton Khirnov Date: Sun, 18 Oct 2020 17:04:09 +0200 Subject: targets/TargetSSHLXCLVM: inherit from TargetSSHLVM Use new parent's _snapshot_lv() to simplify code. --- lbup/_mountinfo.py | 2 +- lbup/targets.py | 108 ++++++++++++++++------------------------------------- 2 files changed, 33 insertions(+), 77 deletions(-) diff --git a/lbup/_mountinfo.py b/lbup/_mountinfo.py index b4d79ae..caa9ddd 100644 --- a/lbup/_mountinfo.py +++ b/lbup/_mountinfo.py @@ -30,7 +30,7 @@ class _MountEntry: mount_opts = None "optional fields, list of bytes" opt_fields = None - "filesystem type, bytes" + "filesystem type, bytes or None" fstype = None "mount source, bytes or None" source = None diff --git a/lbup/targets.py b/lbup/targets.py index 179eeb7..37abe55 100644 --- a/lbup/targets.py +++ b/lbup/targets.py @@ -219,7 +219,7 @@ class TargetSSHLVM(TargetSSH): def __str__(self): return "%s{LVM:%s}" % (super().__str__(), self._snapshot_size) - def _resolve_mntdev(self, ssh, pid = 1): + def _resolve_mntdev(self, mntinfo): """ Find out which LV to snapshot. @@ -229,12 +229,9 @@ class TargetSSHLVM(TargetSSH): Return a tuple of (devnum, mountpoint) """ - # first of all, parse mountinfo - mntinfo = MountInfo( - self._paramiko_exec_cmd(ssh, 'cat /proc/%d/mountinfo' % pid, decode = False)) - devnum = None mountpoint = None + fstype = None for d in self.dirs: mp = mntinfo.mountpoint_for_path(d) e = list(mntinfo.entries_for_mountpoint(mp)) @@ -246,8 +243,9 @@ class TargetSSHLVM(TargetSSH): dn = e[0].devnum if devnum is None: - devnum = dn + devnum = dn mountpoint = mp + fstype = e[0].fstype continue if dn != devnum or mp != mountpoint: @@ -257,7 +255,7 @@ class TargetSSHLVM(TargetSSH): # TODO? check that there are no symlinks? # by running stat maybe? - return (devnum, mountpoint) + return (devnum, mountpoint, fstype) def _resolve_lv(self, ssh, devnum): """ @@ -344,14 +342,16 @@ class TargetSSHLVM(TargetSSH): snapshot_mount = '%s/%s' % (bupdir, 'lbup_mount') self._paramiko_exec_cmd(conn_tgt, 'mkdir -p -m 700 ' + snapshot_mount) - devnum, mountpoint = self._resolve_mntdev(conn_tgt) + # read and parse the mountinfo table + mntinfo = MountInfo( + self._paramiko_exec_cmd(conn_root, 'cat /proc/1/mountinfo', decode = False)) + + devnum, mountpoint, _ = self._resolve_mntdev(mntinfo) self._logger.debug('Backup targets are at device %s, mounted at %s', "%d:%d" % (devnum >> 8, devnum & 255), mountpoint) stack.enter_context(self._mount_snapshot(conn_root, devnum, snapshot_mount)) - save_opts = ['--strip-path', snapshot_mount] - bup_exec = ['bup', 'on', '%s@%s' % (self._remote.username, self._remote.host), '-d', bupdir] reparent = (mountpoint, AbsPath(snapshot_mount)) @@ -360,7 +360,7 @@ class TargetSSHLVM(TargetSSH): save_opts = ['--graft=%s=%s' % (snapshot_mount, mountpoint)], index_opts = ['--no-check-device']) -class TargetSSHLXCLVM(TargetSSH): +class TargetSSHLXCLVM(TargetSSHLVM): """ This target backs up an LXC container that lives on its own LVM logical volume. Requires root-capable login on the container's host. @@ -370,7 +370,6 @@ class TargetSSHLXCLVM(TargetSSH): _parent_remote = None _lxc_username = None _lxc_containername = None - _snapshot_size = None def __init__(self, name, dirs, excludes = None, logger = None, target_remote = None, target_remote_bupdir = None, @@ -385,9 +384,10 @@ class TargetSSHLXCLVM(TargetSSH): self._parent_remote = parent_remote self._lxc_username = lxc_username self._lxc_containername = lxc_containername - self._snapshot_size = snapshot_size - super().__init__(name, dirs, excludes, logger, target_remote, target_remote_bupdir) + super().__init__(name, dirs, excludes, logger, + target_remote, target_remote_bupdir, + snapshot_size) def __str__(self): return "%s{LXC:%s/%s@[%s]}{LVM:%s}" % (super().__str__(), self._lxc_containername, @@ -408,8 +408,6 @@ class TargetSSHLXCLVM(TargetSSH): container_mountpoint = '%s/%s' % (container_bupdir, 'lbup_mount') self._paramiko_exec_cmd(container, 'mkdir -p -m 700 ' + container_mountpoint) - save_opts = ['--strip-path', container_mountpoint] - # get the PID of the container's init cmd_template = 'su -s /bin/sh -c "{command}" %s' % self._lxc_username container_pid = self._paramiko_exec_cmd(parent, cmd_template.format( @@ -418,65 +416,23 @@ class TargetSSHLXCLVM(TargetSSH): if not re.fullmatch('[0-9]+', container_pid): raise BackupException('Invalid container PID: %s' % container_pid) - # get the LV/VG for the container's rootfs - container_rootfs = self._paramiko_exec_cmd(parent, cmd_template.format( - command = 'lxc-info -H -c lxc.rootfs.path -n %s' % - self._lxc_containername)).rstrip('\n') - # oct-escape certain characters as they are in /proc/mounts - # see seq_path[_root]() in linux - container_rootfs = container_rootfs.translate( - { ord(' ') : r'\040', ord('\t') : r'\011', - ord('\n') : r'\012', ord('\\') : r'\0134'}) - # make sure the rootfs path is - # - non-empty - # - an absolute path - # - contains no whitespace - if (len(container_rootfs) <= 1 or container_rootfs[0] != '/' or - re.search(r'\s', container_rootfs)): - raise BackupException('Unxpected container rootfs directory: %s' % container_rootfs) - - # find the device node and the filesystem type for the container rootfs - mountlines = self._paramiko_exec_cmd(parent, - 'grep "%s" /proc/mounts' % container_rootfs).splitlines() - if len(mountlines) != 1: - raise BackupException('Expected exactly one matching mount line for the ' - 'container root, got %d' % len(mountlines)) - - mountline = mountlines[0].split() - if len(mountline) < 2 or mountline[1] != container_rootfs: - raise BackupException('Invalid mount line: %s' % mountline) - lv_path = mountline[0] - lv_fstype = mountline[2] - # make sure the LV path is - # - non-empty - # - an absolute path - # - contains no whitespace - # and that the FS type is non-empty - if (len(lv_path) <= 1 or lv_path[0] != '/' or - re.search(r'\s', lv_path) or len(lv_fstype) < 1): - raise BackupException('Unexpected LV path/FS type: %s\t%s' % (lv_path, lv_fstype)) - - # find the LV and VG names - lvdisplay = self._paramiko_exec_cmd(parent, - 'lvdisplay -C --noheadings -o lv_name,vg_name ' + lv_path).split() - if len(lvdisplay) != 2: - raise BackupException('Unexpected lvdisplay output: %s' % str(lvdisplay)) - lv_name, vg_name = lvdisplay - if len(lv_name) < 1 or len(vg_name) < 1: - raise BackupException('Unexpected LV/VG name: %s\t%s' % (lv_name, vg_name)) - - # create a read-only snapshot with a random name - snapshot_name = secrets.token_urlsafe() - self._paramiko_exec_cmd(parent, - 'lvcreate --permission r --snapshot -L {size} -n {name} {origin}' - .format(size = self._snapshot_size, name = snapshot_name, - origin = lv_path)) - stack.callback(lambda: self._paramiko_exec_cmd(parent, - 'lvremove -f %s/%s' % (vg_name, snapshot_name))) + # read and parse the mountinfo table for the container init + # which runs in the container's mount namespace + mntinfo = MountInfo( + self._paramiko_exec_cmd(parent, 'cat /proc/%s/mountinfo' % container_pid, + decode = False)) + + lv_devnum, lv_mountpoint, lv_fstype = self._resolve_mntdev(mntinfo) + # make sure we have a valid fstype + lv_fstype = lv_fstype.decode('ascii') + if not re.fullmatch(r'\w+', lv_fstype, re.ASCII): + raise BackupException('Invalid LV FS type', lv_fstype) + self._logger.debug('Backup targets are at device %s(%s), mounted at %s', + "%d:%d" % (lv_devnum >> 8, lv_devnum & 255), lv_fstype, lv_mountpoint) + + snapshot_path = stack.enter_context(self._snapshot_lv(parent, lv_devnum)) # execute the backup - # wait for the new node to be created - self._paramiko_exec_cmd(parent, 'udevadm settle') # we cannot trust any binaries located inside the container, since a # compromised container could use them to execute arbitrary code @@ -488,12 +444,12 @@ class TargetSSHLXCLVM(TargetSSH): self._paramiko_exec_cmd(parent, 'nsmount m {pid} {mountpoint} {devpath} {fstype}'.format( pid = container_pid, mountpoint = container_mountpoint, - devpath = '/dev/%s/%s' % (vg_name, snapshot_name), - fstype = lv_fstype)) + devpath = snapshot_path, fstype = lv_fstype)) bup_exec = ['bup', 'on', '%s@%s' % (self._remote.username, self._remote.host), '-d', container_bupdir] - reparent = (ROOT, AbsPath(container_mountpoint)) + save_opts = ['--graft=%s=%s' % (container_mountpoint, lv_mountpoint)] + reparent = (lv_mountpoint, AbsPath(container_mountpoint)) try: ret = self._do_save(bup_exec, dry_run, reparent = reparent, -- cgit v1.2.3