summaryrefslogtreecommitdiff
path: root/bupper/targets.py
diff options
context:
space:
mode:
authorAnton Khirnov <anton@khirnov.net>2020-02-14 13:05:49 +0100
committerAnton Khirnov <anton@khirnov.net>2020-02-14 13:07:53 +0100
commit1c33c917bbcad2924bb722f77c7aaa4f511a299c (patch)
treed6368eeb53488b6e16d558781dbd01f9f5a5c053 /bupper/targets.py
parentb3e182b781935757c0f0aa22e820fa7c0a85ef5d (diff)
targets: further fixes and improvements
Now a static mount directory is used for snapshot mounts. This is necessary due to how bup index works, otherwise it'd see everything as changed on every new backup. Also, extend logging and make the remote bupdir configurable.
Diffstat (limited to 'bupper/targets.py')
-rw-r--r--bupper/targets.py150
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(