From 2ec39a6058d8d778e27945ddb43c6448ed61aabe Mon Sep 17 00:00:00 2001 From: Anton Khirnov Date: Tue, 20 Oct 2020 11:43:55 +0200 Subject: TargetSSH(LXC)LVM: share the save() method --- lbup/targets.py | 116 ++++++++++++++++++++++++++------------------------------ 1 file changed, 53 insertions(+), 63 deletions(-) diff --git a/lbup/targets.py b/lbup/targets.py index 7f85526..59db6db 100644 --- a/lbup/targets.py +++ b/lbup/targets.py @@ -207,6 +207,11 @@ class TargetSSHLVM(TargetSSH): """ _snapshot_size = None + """ + PID of the process whose mount namespace we use. + """ + _mntns_pid = None + def __init__(self, name, dirs, excludes = None, logger = None, remote = None, remote_bupdir = None, snapshot_size = '20G'): self._snapshot_size = snapshot_size @@ -216,6 +221,22 @@ class TargetSSHLVM(TargetSSH): def __str__(self): return "%s{LVM:%s}" % (super().__str__(), self._snapshot_size) + def _root_remote(self): + """ + The the remote for root operations, such as manipulating LVM. + + Overridden in subclasses. + """ + return SSHRemote(self._remote.host, self._remote.port, + 'root', self._remote.proxy_remote) + + def _resolve_mntns(self, ssh): + """ + Get the PID of the process whose mount namespace we use. + Always 1 here, overridden in subclasses. + """ + return 1 + def _resolve_mntdev(self, mntinfo): """ Find out which LV to snapshot. @@ -308,14 +329,16 @@ class TargetSSHLVM(TargetSSH): self._logger.debug('Removed snapshot %s', snapshot_fullname) @contextlib.contextmanager - def _mount_snapshot(self, ssh, devnum, mount_path): + def _mount_snapshot(self, ssh, devnum, mount_path, fstype): """ Return a context manager that creates a read-only LVM snapshot for the specified LV device number and mounts it at mount_path, then unmounts and destroys it at exit. """ with self._snapshot_lv(ssh, devnum) as lv_path: - self._paramiko_exec_cmd(ssh, 'mount -oro %s %s' % (lv_path, mount_path)) + self._paramiko_exec_cmd(ssh, + 'mount -t{fstype} -oro {lv_path} {mount_path}'.format( + fstype = fstype, lv_path = lv_path, mount_path = mount_path)) try: yield None finally: @@ -325,10 +348,7 @@ class TargetSSHLVM(TargetSSH): def save(self, dry_run = False): with contextlib.ExitStack() as stack: conn_tgt = stack.enter_context(_ssh_client.SSHConnection(self._remote)) - - remote_root = SSHRemote(self._remote.host, self._remote.port, - 'root', self._remote.proxy_remote) - conn_root = stack.enter_context(_ssh_client.SSHConnection(remote_root)) + conn_root = stack.enter_context(_ssh_client.SSHConnection(self._root_remote())) # resolve the path to BUP_DIR on the remote bupdir = self._resolve_remote_bupdir(conn_tgt) @@ -340,15 +360,24 @@ class TargetSSHLVM(TargetSSH): snapshot_mount = '%s/%s' % (bupdir, 'lbup_mount') self._paramiko_exec_cmd(conn_tgt, 'mkdir -p -m 700 ' + snapshot_mount) + self._mntns_pid = self._resolve_mntns(conn_root) + # read and parse the mountinfo table mntinfo = MountInfo( - self._paramiko_exec_cmd(conn_root, 'cat /proc/1/mountinfo', decode = False)) + self._paramiko_exec_cmd(conn_root, 'cat /proc/%d/mountinfo' % self._mntns_pid, + decode = False)) + + devnum, mountpoint, fstype = self._resolve_mntdev(mntinfo) - 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) + # 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('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, devnum, snapshot_mount)) + stack.enter_context(self._mount_snapshot(conn_root, devnum, snapshot_mount, fstype)) bup_exec = ['bup', 'on', '%s@%s' % (self._remote.username, self._remote.host), '-d', bupdir] @@ -396,8 +425,18 @@ class TargetSSHLXCLVM(TargetSSHLVM): return "%s{LXC:%s/%s@[%s]}{LVM:%s}" % (super().__str__(), self._lxc_containername, self._lxc_username, str(self._parent_remote), self._snapshot_size) + def _resolve_mntns(self, ssh): + # get the PID of the container's init + cmd = 'su -s /bin/sh -c "lxc-info -H -p -n {container}" {user}'.format( + container = self._lxc_containername, user = self._lxc_username) + container_pid = self._paramiko_exec_cmd(ssh, cmd).rstrip('\n') + return int(container_pid) + + def _root_remote(self): + return self._parent_remote + @contextlib.contextmanager - def _nsmount_snapshot(self, ssh, devnum, mount_path, container_pid, fstype): + def _mount_snapshot(self, ssh, devnum, mount_path, fstype): """ Return a context manager that creates a read-only LVM snapshot for the specified LV device number and mounts it at mount_path @@ -413,60 +452,11 @@ class TargetSSHLXCLVM(TargetSSHLVM): # container mount namespace self._paramiko_exec_cmd(ssh, 'nsmount m {pid} {mount_path} {lv_path} {fstype}'.format( - pid = container_pid, mount_path = mount_path, + pid = self._mntns_pid, mount_path = mount_path, lv_path = lv_path, fstype = fstype)) try: yield None finally: self._paramiko_exec_cmd(ssh, 'nsmount u {pid} {mount_path}'.format( - pid = container_pid, mount_path = mount_path)) - - def save(self, dry_run = False): - with contextlib.ExitStack() as stack: - parent = stack.enter_context(_ssh_client.SSHConnection(self._parent_remote)) - container = stack.enter_context(_ssh_client.SSHConnection(self._remote)) - - # resolve the path to BUP_DIR on the container - container_bupdir = self._resolve_remote_bupdir(container) - - # 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 - container_mountpoint = '%s/%s' % (container_bupdir, 'lbup_mount') - self._paramiko_exec_cmd(container, 'mkdir -p -m 700 ' + 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( - command = 'lxc-info -H -p -n %s' % self._lxc_containername)).rstrip('\n') - # make sure it's a number - if not re.fullmatch('[0-9]+', container_pid): - raise BackupException('Invalid container PID: %s' % container_pid) - - # 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) - - stack.enter_context(self._nsmount_snapshot(parent, lv_devnum, container_mountpoint, - container_pid, lv_fstype)) - - # execute the backup - bup_exec = ['bup', 'on', '%s@%s' % (self._remote.username, self._remote.host), - '-d', container_bupdir] - save_opts = ['--graft=%s=%s' % (container_mountpoint, lv_mountpoint)] - reparent = (lv_mountpoint, AbsPath(container_mountpoint)) - return self._do_save(bup_exec, dry_run, - reparent = reparent, - save_opts = save_opts, index_opts = ['--no-check-device']) + pid = self._mntns_pid, mount_path = mount_path)) -- cgit v1.2.3