diff options
Diffstat (limited to 'bupper/targets.py')
-rw-r--r-- | bupper/targets.py | 150 |
1 files changed, 90 insertions, 60 deletions
diff --git a/bupper/targets.py b/bupper/targets.py index 0df7105..1485690 100644 --- a/bupper/targets.py +++ b/bupper/targets.py @@ -39,11 +39,6 @@ class Target(ABC): _logger = None - _index_opts = None - _save_opts = None - - _path_prefix = '' - def __init__(self, name, dirs, excludes = None, logger = None): if excludes is None: excludes = [] @@ -57,24 +52,51 @@ class Target(ABC): else: self._logger = logger - self._index_opts = [] - self._save_opts = [] + def _log_command(self, name, retcode, stdout, stderr): + self._logger.debug('%s finished with return code %d' % (name, retcode)) + + def sanitize(b): + LOG_LEN = 128 + # truncate and decode + s = b[:LOG_LEN].decode('utf-8', errors = 'backslashreplace') + # replace newlines with literal \n's + s = r'\n'.join(s.splitlines()) + # add ellipsis if truncated + if len(b) > LOG_LEN: + s += '[...]' + + return s - 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] + if len(stdout) > 0: + self._logger.debug('%s stdout: %s' % (name, sanitize(stdout))) + if len(stderr) > 0: + self._logger.debug('%s stderr: %s' % (name, sanitize(stderr))) + + def _do_save(self, bup_exec, path_prefix = '', index_opts = None, save_opts = None): + excludes = [path_prefix + '/' + e for e in self.excludes] + dirs = [path_prefix + '/' + d for d in self.dirs] + + if index_opts is None: + index_opts = [] + if save_opts is None: + save_opts = [] # index - cmd = bup_exec + ['index', '--update', '--one-file-system'] + self._index_opts + cmd = bup_exec + ['index', '--update', '--one-file-system'] + index_opts 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) + self._log_command('Index', res_idx.returncode, + res_idx.stdout, res_idx.stderr) # save - cmd = bup_exec + ['save', '-n', self.name] + self._save_opts + dirs + cmd = bup_exec + ['save', '-n', self.name] + save_opts + dirs self._logger.debug('Executing save command: ' + str(cmd)) res_save = subprocess.run(cmd, capture_output = True) + self._log_command('Save', res_save.returncode, + res_save.stdout, res_save.stderr) retcode = 0 output = b'' @@ -101,7 +123,7 @@ class TargetSSH(Target): _remote = None def __init__(self, name, dirs, excludes = None, logger = None, - remote = None): + remote = None, remote_bupdir = None): if remote is None: remote = _parse_name(name) if remote.proxy_remote is not None: @@ -110,9 +132,47 @@ class TargetSSH(Target): raise NotImplementedError('Specifying port not implemented') self._remote = remote + if remote_bupdir is None: + remote_bupdir = '$HOME/.bup' + self._remote_bupdir = remote_bupdir + super().__init__(name, dirs, excludes, logger) + def _paramiko_exec_cmd(self, client, cmd): + self._logger.debug('Client %s: executing command: %s' % (client, cmd)) + + res = client.exec_command(cmd) + + chan = res[0].channel + chan.settimeout(64) + try: + out, err = res[1].read(), res[2].read() + except socket.timeout as t: + raise RemoteExecException('Timeout waiting for command output', + errno.ETIMEDOUT, b'') from t + + chan.recv_exit_status() + if chan.exit_status != 0: + raise RemoteExecException('Error executing "%s"' % cmd, + chan.exit_status, err + out) + + self._log_command('Remote command', chan.exit_status, out, err) + + return out.decode('utf-8', errors = 'backslashreplace') + + def _resolve_remote_bupdir(self, ssh): + bupdir = self._paramiko_exec_cmd(ssh, 'realpath -e ' + self._remote_bupdir).splitlines() + if (len(bupdir) != 1 or len(bupdir[0]) <= 1 or bupdir[0][0] != '/' or + re.search(r'\s', bupdir[0])): + raise BackupException('Invalid BUP_DIR on the remote target: %s' % str(bupdir)) + return bupdir[0] + def save(self, data_dir): + with _ssh_client.SSHConnection(self._remote) as ssh: + remote_bupdir = self._resolve_bupdir(ssh) + + bup_exec = ['bup', 'on', '%s@%s' % (self._remote.username, self._remote.host), + '-d', remote_bupdir] return self._do_save(['bup', 'on', '%s@%s' % (self._remote.username, self._remote.host)]) class TargetSSHLXCLVM(TargetSSH): @@ -127,7 +187,8 @@ class TargetSSHLXCLVM(TargetSSH): _lxc_containername = None def __init__(self, name, dirs, excludes = None, logger = None, - target_remote = None, parent_remote = None, + target_remote = None, target_remote_bupdir = None, + parent_remote = None, lxc_username = None, lxc_containername = None, snapshot_size = '20G'): if parent_remote is None: @@ -140,58 +201,24 @@ 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)) - - res = client.exec_command(cmd) - - chan = res[0].channel - chan.settimeout(64) - try: - out, err = res[1].read(), res[2].read() - except socket.timeout as t: - raise RemoteExecException('Timeout waiting for command output', - errno.ETIMEDOUT, b'') from t - - chan.recv_exit_status() - if chan.exit_status != 0: - raise RemoteExecException('Error executing "%s"' % cmd, - chan.exit_status, err + out) - - 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 - + super().__init__(name, dirs, excludes, logger, target_remote, target_remote_bupdir) def save(self, data_dir): with contextlib.ExitStack() as stack: parent = stack.enter_context(_ssh_client.SSHConnection(self._parent_remote)) container = stack.enter_context(_ssh_client.SSHConnection(self._remote)) - # create the mount directory - container_mountpoint = self._paramiko_exec_cmd(container, - 'mktemp -d --tmpdir bupper.XXXXXXXX').rstrip('\n') - # 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' % container_mountpoint)) + # 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/bupper_mount + container_mountpoint = '%s/%s' % (container_bupdir, 'bupper_mount') + self._paramiko_exec_cmd(container, 'mkdir -p -m 700 ' + container_mountpoint) - self._path_prefix = container_mountpoint - self._save_opts.extend(['--strip-path', 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 @@ -274,8 +301,11 @@ class TargetSSHLXCLVM(TargetSSH): devpath = '/dev/%s/%s' % (vg_name, snapshot_name), fstype = lv_fstype)) + bup_exec = ['bup', 'on', '%s@%s' % (self._remote.username, self._remote.host), + '-d', container_bupdir] try: - ret = super().save(data_dir) + ret = self._do_save(bup_exec, path_prefix = container_mountpoint, + save_opts = save_opts, index_opts = ['--no-check-device']) finally: self._paramiko_exec_cmd(parent, 'nsmount u {pid} {mountpoint}'.format( |