summaryrefslogtreecommitdiff
path: root/bupper/targets.py
diff options
context:
space:
mode:
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(