From b3e182b781935757c0f0aa22e820fa7c0a85ef5d Mon Sep 17 00:00:00 2001 From: Anton Khirnov Date: Fri, 14 Feb 2020 11:37:28 +0100 Subject: lxvlvm: actually read from the snapshot Also, check all the remote outputs more thoroughly. --- bupper/targets.py | 110 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 74 insertions(+), 36 deletions(-) diff --git a/bupper/targets.py b/bupper/targets.py index 65fc22e..0df7105 100644 --- a/bupper/targets.py +++ b/bupper/targets.py @@ -3,7 +3,6 @@ from abc import ABC, abstractmethod import contextlib import errno import logging -import os.path import re import socket import subprocess @@ -43,6 +42,8 @@ class Target(ABC): _index_opts = None _save_opts = None + _path_prefix = '' + def __init__(self, name, dirs, excludes = None, logger = None): if excludes is None: excludes = [] @@ -60,13 +61,18 @@ class Target(ABC): self._save_opts = [] def _do_save(self, bup_exec): + excludes = [self._path_prefix + '/' + e for e in self.excludes] + dirs = [self._path_prefix + '/' + d for d in self.dirs] + + # index cmd = bup_exec + ['index', '--update', '--one-file-system'] + self._index_opts - cmd.extend(['--exclude=%s' % e for e in self.excludes]) - cmd.extend(self.dirs) + cmd.extend(['--exclude=%s' % e for e in excludes]) + cmd.extend(dirs) self._logger.debug('Executing index command: ' + str(cmd)) res_idx = subprocess.run(cmd, capture_output = True) - cmd = bup_exec + ['save', '-n', self.name] + self._save_opts + self.dirs + # save + cmd = bup_exec + ['save', '-n', self.name] + self._save_opts + dirs self._logger.debug('Executing save command: ' + str(cmd)) res_save = subprocess.run(cmd, capture_output = True) @@ -94,7 +100,7 @@ class TargetLocal(Target): class TargetSSH(Target): _remote = None - def __init__(self, name, dirs, excludes = None, + def __init__(self, name, dirs, excludes = None, logger = None, remote = None): if remote is None: remote = _parse_name(name) @@ -104,7 +110,7 @@ class TargetSSH(Target): raise NotImplementedError('Specifying port not implemented') self._remote = remote - super().__init__(name, dirs, excludes) + super().__init__(name, dirs, excludes, logger) def save(self, data_dir): return self._do_save(['bup', 'on', '%s@%s' % (self._remote.username, self._remote.host)]) @@ -120,19 +126,10 @@ class TargetSSHLXCLVM(TargetSSH): _lxc_username = None _lxc_containername = None - _container_mountpoint = '/mnt/bupper' - - def __init__(self, name, dirs, excludes = None, + def __init__(self, name, dirs, excludes = None, logger = None, target_remote = None, parent_remote = None, lxc_username = None, lxc_containername = None, snapshot_size = '20G'): - dirs_snapshot = [os.path.join(self._container_mountpoint, d) for d in dirs] - excludes_snapshot = None - if excludes is not None: - excludes_snapshot = [os.path.join(self._container_mountpoint, e) for e in excludes] - - super().__init__(name, dirs_snapshot, excludes_snapshot, target_remote) - if parent_remote is None: raise ValueError('parent_remote not specified') if lxc_username is None: @@ -143,6 +140,8 @@ class TargetSSHLXCLVM(TargetSSH): self._lxc_containername = lxc_containername self._snapshot_size = snapshot_size + super().__init__(name, dirs, excludes, logger, target_remote) + def _paramiko_exec_cmd(self, client, cmd): self._logger.debug('Client %s: executing command: %s' % (client, cmd)) @@ -160,7 +159,17 @@ class TargetSSHLXCLVM(TargetSSH): if chan.exit_status != 0: raise RemoteExecException('Error executing "%s"' % cmd, chan.exit_status, err + out) - return out.decode('utf-8', errors = 'backslashreplace') + + out = out.decode('utf-8', errors = 'backslashreplace') + err = err.decode('utf-8', errors = 'backslashreplace') + + self._logger.debug('Command successful.') + if len(out): + self._logger.debug('Command stdout: %s' % out) + if len(err): + self._logger.debug('Command stderr: %s' % err) + + return out def save(self, data_dir): @@ -169,44 +178,73 @@ class TargetSSHLXCLVM(TargetSSH): container = stack.enter_context(_ssh_client.SSHConnection(self._remote)) # create the mount directory - self._container_mountpoint = self._paramiko_exec_cmd(container, + container_mountpoint = self._paramiko_exec_cmd(container, 'mktemp -d --tmpdir bupper.XXXXXXXX').rstrip('\n') - if len(self._container_mountpoint) <= 1 or self._container_mountpoint[0] != '/': - raise BackupException('Unexpected mount directory: %s' % self._container_mountpoint) + # make sure it's + # - non-empty + # - an absolute path + # - contains no whitespace + if (len(container_mountpoint) <= 1 or container_mountpoint[0] != '/' or + re.search(r'\s', container_mountpoint)): + raise BackupException('Unexpected mount directory: %s' % container_mountpoint) stack.callback(lambda: self._paramiko_exec_cmd(container, - 'rmdir %s' % self._container_mountpoint)) + 'rmdir %s' % container_mountpoint)) - self._save_opts.extend(['--strip-path', self._container_mountpoint]) + self._path_prefix = container_mountpoint + self._save_opts.extend(['--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( 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) # 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')\ - .translate({ord(' ') : r'\040', ord('\t') : r'\011', - ord('\n') : r'\012', ord('\\') : r'\0134'}) - if len(container_rootfs) <= 1 or container_rootfs[0] != '/': + 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) - mountline = self._paramiko_exec_cmd(parent, 'grep "%s" /proc/mounts' % - container_rootfs).rstrip('\n').split() + # 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] - if len(lv_path) <= 1 or lv_path[0] != '/' or len(lv_fstype) < 1: + # 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)) - lv_name, vg_name = self._paramiko_exec_cmd(parent, - 'lvdisplay -C --noheadings -o lv_name,vg_name ' + lv_path)\ - .strip().split() + # 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)) @@ -232,7 +270,7 @@ class TargetSSHLXCLVM(TargetSSH): # container mount namespace self._paramiko_exec_cmd(parent, 'nsmount m {pid} {mountpoint} {devpath} {fstype}'.format( - pid = container_pid, mountpoint = self._container_mountpoint, + pid = container_pid, mountpoint = container_mountpoint, devpath = '/dev/%s/%s' % (vg_name, snapshot_name), fstype = lv_fstype)) @@ -241,6 +279,6 @@ class TargetSSHLXCLVM(TargetSSH): finally: self._paramiko_exec_cmd(parent, 'nsmount u {pid} {mountpoint}'.format( - pid = container_pid, mountpoint = self._container_mountpoint)) + pid = container_pid, mountpoint = container_mountpoint)) return ret -- cgit v1.2.3