summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Khirnov <anton@khirnov.net>2020-10-18 17:04:09 +0200
committerAnton Khirnov <anton@khirnov.net>2020-10-18 17:04:09 +0200
commit21c759115774e7ab980b210d6f806938fa0b12e5 (patch)
tree973f655c1dc46bf4da5ba11543fe5dbc266182a9
parentac612f7daed790b6f39e2a7358799a307f4aefce (diff)
targets/TargetSSHLXCLVM: inherit from TargetSSHLVM
Use new parent's _snapshot_lv() to simplify code.
-rw-r--r--lbup/_mountinfo.py2
-rw-r--r--lbup/targets.py108
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,