diff options
author | Anton Khirnov <anton@khirnov.net> | 2020-02-13 21:39:41 +0100 |
---|---|---|
committer | Anton Khirnov <anton@khirnov.net> | 2020-02-13 21:39:41 +0100 |
commit | f1ef9c053c84515761f59412a91ca3adbc8e7392 (patch) | |
tree | 9adfb42a73cf1aa61556d16ee5665e6c3f611dd4 /targets.py | |
parent | b476a22f4a85d90ccaec2ee27a528d5bfe428a82 (diff) |
Move the python files to a package.
Diffstat (limited to 'targets.py')
-rw-r--r-- | targets.py | 186 |
1 files changed, 0 insertions, 186 deletions
diff --git a/targets.py b/targets.py deleted file mode 100644 index e56b71b..0000000 --- a/targets.py +++ /dev/null @@ -1,186 +0,0 @@ - -from abc import ABC, abstractmethod -import re -import subprocess - -from .exceptions import RemoteExecException -from . import repository -from . import ssh_remote -from . import _ssh_client - -def _parse_name(name): - """ - Parse a backup name into a remote specification. - """ - # split off the username - if not '@' in name: - raise ValueError('Invalid backup name: "%s", must be of format user@host') - username, _, host = name.partition('@') - - port = 22 # overridden later if specified in name - colons = host.count(':') - if colons >= 2: # IPv6 literal, possibly with port - m = re.match(r'\[(.+)\](:\d+)?', host, re.ASCII | re.IGNORECASE) - if m is not None: # [literal]:port - host, port = m.groups() - elif colons == 1: # host:port - host, _, port = host.partition(':') - - return ssh_remote.SSHRemote(host, port, username) - -class Target(ABC): - name = None - dirs = None - excludes = None - def __init__(self, name, dirs, excludes = None): - if excludes is None: - excludes = [] - - self.name = name - self.dirs = dirs - self.excludes = excludes - - @abstractmethod - def save(self, data_dir): - pass - -class TargetLocal(Target): - def save(self, data_dir): - cmd = ['bup', 'index', '--update', '--one-file-system'] - cmd.extend(['--exclude=%s' % e for e in self.excludes]) - cmd.extend(self.dirs) - res_idx = subprocess.run(cmd, capture_output = True) - - cmd = ['bup', 'save', '-n', self.name] + self.dirs - res_save = subprocess.run(cmd, capture_output = True) - - retcode = 0 - output = b'' - if res_idx.returncode != 0: - retcode = res_idx.returncode - output += res_idx.stderr + res_idx.stdout - if res_save.returncode != 0: - retcode = res_save.returncode - output += res_save.stderr + res_save.stdout - - result = repository.StepResult(retcode, output) - - return result - -class TargetSSH(Target): - _remote = None - - def __init__(self, name, dirs, excludes = None, - remote = None): - super().__init__(name, dirs, excludes) - - if remote is None: - remote = _parse_name(name) - if remote.proxy_remote is not None: - raise NotImplementedError('Proxy remote not implemented') - if remote.port != 22: - raise NotImplementedError('Specifying port not implemented') - self._remote = remote - - def save(self, data_dir): - cmd = ['bup', 'on', '%s@%s' % (self._remote.username, self._remote.host), 'index', '--update', '--one-file-system'] - cmd.extend(['--exclude=%s' % e for e in self.excludes]) - cmd.extend(self.dirs) - res_idx = subprocess.run(cmd, capture_output = True) - - cmd = ['bup', 'on', '%s@%s' %(self._remote.username, self._remote.host), 'save', '-n', self.name] + self.dirs - res_save = subprocess.run(cmd, capture_output = True) - - retcode = 0 - output = b'' - if res_idx.returncode != 0: - retcode = res_idx.returncode - output += res_idx.stderr + res_idx.stdout - if res_save.returncode != 0: - retcode = res_save.returncode - output += res_save.stderr + res_save.stdout - - result = repository.StepResult(retcode, output) - - return result - -def _paramiko_exec_cmd(client, cmd): - res = client.exec_command(cmd) - chan = res[0].channel - out, err = res[1].read(), res[2].read() - if chan.exit_status != 0: - raise RemoteExecException('Error executing "%s"' % cmd, - chan.exit_status, err + out) - return out.decode('utf-8', errors = 'backslashreplace') - -class TargetSSHLXCLVM(TargetSSH): - """ - This target backs up an LXC container that lives on its own LVM logical - volume. Requires root-capable login on the container's host. - - :param SSHRemote parent_remote: - """ - _parent_remote = None - _lxc_username = None - _lxc_containername = None - - def __init__(self, name, dirs, excludes = None, - target_remote = None, parent_remote = None, - lxc_username = None, lxc_containername = None, - snapshot_size = '20G'): - super().__init__(name, dirs, excludes, target_remote) - - if parent_remote is None: - raise ValueError('parent_remote not specified') - if lxc_username is None: - lxc_username = parent_remote.usename - - self._parent_remote = parent_remote - self._lxc_username = lxc_username - self._lxc_containername = lxc_containername - self._snapshot_size = snapshot_size - - def save(self, data_dir): - with (_ssh_client.SSHConnection(self._parent_remote) as parent, - _ssh_client.SSHConnection(self._remote) as container): - # get the PID of the container's init - cmd_template = 'su -s /bin/sh -c "{command}" %s' % self._lxc_username - container_pid = _paramiko_exec_cmd(parent, cmd_template.format( - command = 'lxc-info -H -p -n %s' % self._lxc_containername)).rstrip('\n') - - # get the LV/VG for the container's rootfs - container_rootfs = _paramiko_exec_cmd(parent, cmd_template.format( - command = 'lxc-info -H -c lxc.rootfs -n %s' % - self._lxc_containername))\ - .rstrip('\n')\ - .translate({ord(' ') : r'\040', ord('\t') : r'\011', - ord('\n') : r'\012', ord('\\') : r'\O134'}) - mountline = _paramiko_exec_cmd(parent, 'grep "%s" /proc/mounts' % - container_rootfs).rstrip('\n').split() - if len(mountline) < 2 or mountline[1] != container_rootfs: - raise RemoteExecException('Invalid mount line: %s' % mountline) - lv_path = mountline[0] - lv_name, vg_name = _paramiko_exec_cmd(parent, - 'lvdisplay -C --noheadings -o lv_name,vg_name ' + lv_path)\ - .strip().split() - - # we cannot trust any binaries located inside the container, since a - # compromised container could use them to execute arbitrary code - # with real root privileges, thus nullifying the point of - # unprivileged containers) - # so we now create a temporary - - - # create a read-only snapshot - snapshot_name = 'bupper_' + lv_name - _paramiko_exec_cmd(parent, - 'lvcreate --permission r --snapshot -L {size} -n {name} {origin}' - .format(size = self._snapshot_size, name = snapshot_name, - origin = lv_path)) - - # execute the backup - try: - print(container_pid, vg_name, lv_path, snapshot_name) - finally: - # delete the snapshot - _paramiko_exec_cmd(parent, 'lvremove -f %s/%s' % (vg_name, snapshot_name)) |