def checkFuse(self): """ Check if command in self.mountproc is installed and user is part of group ``fuse``. Raises: exceptions.MountException: if either command is not available or user is not in group fuse """ logger.debug('Check fuse', self) if not tools.checkCommand(self.mountproc): logger.debug('%s is missing' % self.mountproc, self) raise MountException( _('%(proc)s not found. Please install e.g. %(install_command)s' ) % { 'proc': self.mountproc, 'install_command': "'apt-get install %s'" % self.mountproc }) if self.CHECK_FUSE_GROUP: user = self.config.user() try: fuse_grp_members = grp.getgrnam('fuse')[3] except KeyError: #group fuse doesn't exist. So most likely it isn't used by this distribution logger.debug("Group fuse doesn't exist. Skip test", self) return if not user in fuse_grp_members: logger.debug('User %s is not in group fuse' % user, self) raise MountException( _('%(user)s is not member of group \'fuse\'.\n ' 'Run \'sudo adduser %(user)s fuse\'. To apply ' 'changes logout and login again.\nLook at ' '\'man backintime\' for further instructions.') % {'user': user})
def isConfigured(self): """ check if encfs config file exist. If not and if we are in settingsdialog ask for password confirmation. _mount will then create a new config """ cfg = self.configFile() if os.path.isfile(cfg): logger.debug('Found encfs config in %s' % cfg, self) return True else: logger.debug('No encfs config in %s' % cfg, self) msg = _('Config for encrypted folder not found.') if not self.tmp_mount: raise MountException(msg) else: if not self.config.askQuestion( msg + _('\nCreate a new encrypted folder?')): raise MountException(_('Cancel')) else: pw = password.Password(self.config) password_confirm = pw.passwordFromUser( self.parent, prompt=_('Please confirm password')) if self.password == password_confirm: return False else: raise MountException(_('Password doesn\'t match'))
def checkRemoteFolder(self): """ Check the remote path. If the remote path doesn't exist this will create it. If it already exist this will check, that it is a folder and has correct permissions. Raises: exceptions.MountException: if remote path couldn't be created or doesn't have correct permissions. """ logger.debug('Check remote folder', self) cmd = 'd=0;' cmd += 'test -e "%s" || d=1;' % self.path #path doesn't exist. set d=1 to indicate cmd += 'test $d -eq 1 && mkdir "%s"; err=$?;' % self.path #create path, get errorcode from mkdir cmd += 'test $d -eq 1 && exit $err;' #return errorcode from mkdir cmd += 'test -d "%s" || exit 11;' % self.path #path is no directory cmd += 'test -w "%s" || exit 12;' % self.path #path is not writable cmd += 'test -x "%s" || exit 13;' % self.path #path is not executable cmd += 'exit 20' #everything is fine ssh = self.config.sshCommand( cmd=[cmd], custom_args=['-p', str(self.port), self.user_host], port=False, cipher=False, user_host=False, nice=False, ionice=False, profile_id=self.profile_id) logger.debug('Call command: %s' % ' '.join(ssh), self) proc = subprocess.Popen(ssh, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) proc.communicate() if proc.returncode: logger.debug('Command returncode: %s' % proc.returncode, self) if proc.returncode == 20: #clean exit pass elif proc.returncode == 11: raise MountException( _('Remote path exists but is not a directory:\n %s') % self.path) elif proc.returncode == 12: raise MountException( _('Remote path is not writable:\n %s') % self.path) elif proc.returncode == 13: raise MountException( _('Remote path is not executable:\n %s') % self.path) else: raise MountException( _('Couldn\'t create remote path:\n %s') % self.path) else: #returncode is 0 logger.info('Create remote folder %s' % self.path, self)
def checkLogin(self): """ Try to login to remote host with public/private-key-method (passwordless). Raises: exceptions.MountException: if login failed """ logger.debug('Check login', self) ssh = self.config.sshCommand(cmd=['echo', '"Hello"'], custom_args=[ '-o', 'PreferredAuthentications=publickey', '-p', str(self.port), self.user_host ], port=False, cipher=False, user_host=False, nice=False, ionice=False, profile_id=self.profile_id) proc = subprocess.Popen(ssh, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, universal_newlines=True) err = proc.communicate()[1] if proc.returncode: raise MountException( _('Password-less authentication for %(user)s@%(host)s ' 'failed. Look at \'man backintime\' for further ' 'instructions.') % { 'user': self.user, 'host': self.host } + '\n\n' + err)
def checkPingHost(self): """ Check if the remote host is online. Other than methods name may let suppose this does not use Ping (``ICMP``) but try to open a connection to the configured port on the remote host. In this way it will even work on remote hosts which have ``ICMP`` disabled. If connection failed it will retry five times before failing. Raises: exceptions.MountException: if connection failed most probably because remote host is offline """ if not self.config.sshCheckPingHost(self.profile_id): return logger.debug('Check ping host', self) count = 0 while count < 5: try: with socket.create_connection((self.host, self.port), 2.0) as s: result = s.connect_ex(s.getpeername()) except: result = -1 if result == 0: logger.debug('Host %s is available' % self.host, self) return logger.debug('Could not ping host %s. Try again' % self.host, self) count += 1 sleep(0.2) if result != 0: logger.debug('Failed pinging host %s' % self.host, self) raise MountException( _('Ping %s failed. Host is down or wrong address.') % self.host)
def _mount(self): """ mount the service """ if self.password is None: self.password = self.config.password(self.parent, self.profile_id, self.mode) logger.debug('Provide password through temp FIFO', self) thread = password_ipc.TempPasswordThread(self.password) env = self.env() env['ASKPASS_TEMP'] = thread.temp_file thread.start() encfs = [self.mountproc, '--extpass=backintime-askpass'] if self.reverse: encfs += ['--reverse'] if not self.isConfigured(): encfs += ['--standard'] encfs += [self.path, self.currentMountpoint] logger.debug('Call mount command: %s' % ' '.join(encfs), self) proc = subprocess.Popen(encfs, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True) output = proc.communicate()[0] self.backupConfig() if proc.returncode: raise MountException(_('Can\'t mount \'%(command)s\':\n\n%(error)s') \ % {'command': ' '.join(encfs), 'error': output}) thread.stop()
def _mount(self): """ Backend mount method. This will call ``sshfs`` to mount the remote path. Raises: exceptions.MountException: if mount wasn't successful """ sshfs = [self.mountproc] sshfs += self.config.sshDefaultArgs(self.profile_id) sshfs += ['-p', str(self.port)] if not self.cipher == 'default': sshfs.extend(['-o', 'Ciphers=%s' % self.cipher]) sshfs.extend([ '-o', 'idmap=user', '-o', 'cache_dir_timeout=2', '-o', 'cache_stat_timeout=2' ]) sshfs.extend([self.user_host_path, self.currentMountpoint]) #bugfix: sshfs doesn't mount if locale in LC_ALL is not available on remote host #LANG or other envirnoment variable are no problem. env = os.environ.copy() if 'LC_ALL' in list(env.keys()): env['LC_ALL'] = 'C' logger.debug('Call mount command: %s' % ' '.join(sshfs), self) proc = subprocess.Popen(sshfs, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, universal_newlines=True) err = proc.communicate()[1] if proc.returncode: raise MountException( _('Can\'t mount %s') % ' '.join(sshfs) + '\n\n' + err)
def checkCipher(self): """ Try to login to remote host with the choosen cipher. This should make sure both `localhost` and the remote host support the choosen cipher. Raises: exceptions.MountException: if login with the cipher failed """ if not self.cipher == 'default': logger.debug('Check cipher', self) ssh = self.config.sshCommand(cmd = ['echo', '"Hello"'], custom_args = ['-o', 'Ciphers=%s' % self.cipher, '-p', str(self.port), self.user_host], port = False, cipher = False, user_host = False, nice = False, ionice = False, profile_id = self.profile_id) proc = subprocess.Popen(ssh, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, universal_newlines = True) err = proc.communicate()[1] if proc.returncode: logger.debug('Ciper %s is not supported' %self.config.SSH_CIPHERS[self.cipher], self) raise MountException(_('Cipher %(cipher)s failed for %(host)s:\n%(err)s') % {'cipher' : self.config.SSH_CIPHERS[self.cipher], 'host' : self.host, 'err' : err})
def mountProcessLockAcquire(self, timeout=60): """ Create a short term lock only for blocking other processes changing mounts at the same time. Args: timeout (int): wait ``timeout`` seconds before fail acquiring the lock Raises: exceptions.MountException: if timed out """ lock_path = self.mount_root lockSuffix = '.lock' lock = os.path.join(lock_path, self.pid + lockSuffix) count = 0 while self.checkLocks(lock_path, lockSuffix): count += 1 if count == timeout: raise MountException(_('Mountprocess lock timeout')) sleep(1) logger.debug('Acquire mountprocess lock %s' % lock, self) with open(lock, 'w') as f: f.write(self.pid)
def _umount(self): """ umount the service """ try: subprocess.check_call(['fusermount', '-u', self.mountpoint]) except subprocess.CalledProcessError: raise MountException( _('Can\'t unmount sshfs %s') % self.mountpoint)
def startSshAgent(self): """ Start a new ``ssh-agent`` if it is not already running. Raises: exceptions.MountException: if starting ``ssh-agent`` failed """ SOCK = 'SSH_AUTH_SOCK' PID = 'SSH_AGENT_PID' if os.getenv(SOCK, '') and os.getenv(PID, ''): logger.debug('ssh-agent already running. Skip starting a new one.', self) return sshAgent = tools.which('ssh-agent') if not sshAgent: raise MountException( 'ssh-agent not found. Please make sure it is installed.') if isinstance(sshAgent, str): sshAgent = [ sshAgent, ] sa = subprocess.Popen(sshAgent, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) out, err = sa.communicate() if sa.returncode: raise MountException('Failed to start ssh-agent: [{}] {}'.format( sa.returncode, err)) m = re.match( r'.*{}(?:=|\s)([^;]+);.*{}(?:=|\s)(\d+);'.format(SOCK, PID), out, re.DOTALL | re.MULTILINE) if m: logger.debug( 'ssh-agent started successful: {}={} | {}={}'.format( SOCK, m.group(1), PID, m.group(2)), self) os.environ[SOCK] = m.group(1) os.environ[PID] = m.group(2) atexit.register(os.kill, int(m.group(2)), signal.SIGKILL) else: raise MountException( 'No matching output from ssh-agent: {} | {}'.format(out, err))
def is_mounted(self): """ return True if path is is already mounted """ if os.path.ismount(self.mountpoint): return True else: if os.listdir(self.mountpoint): raise MountException( _('mountpoint %s not empty.') % self.mountpoint) return False
def maxArg(): if retry: raise MountException("Checking commands on remote host didn't return any output. " "We already checked the maximum argument lenght but it seem like " "there is an other problem") logger.warning('Looks like the command was to long for remote SSHd. We will test max arg length now and retry.', self) import sshMaxArg mid = sshMaxArg.test_ssh_max_arg(self.user_host) sshMaxArg.reportResult(self.host, mid) self.config.set_ssh_max_arg_length(mid, self.profile_id) return self.check_remote_commands(retry = True)
def check_remote_folder(self): """ check if remote folder exists and is write- and executable. Create folder if it doesn't exist. """ logger.debug('Check remote folder', self) cmd = 'd=0;' cmd += 'test -e %s || d=1;' % self.path #path doesn't exist. set d=1 to indicate cmd += 'test $d -eq 1 && mkdir %s; err=$?;' % self.path #create path, get errorcode from mkdir cmd += 'test $d -eq 1 && exit $err;' #return errorcode from mkdir cmd += 'test -d %s || exit 11;' % self.path #path is no directory cmd += 'test -w %s || exit 12;' % self.path #path is not writeable cmd += 'test -x %s || exit 13;' % self.path #path is not executable cmd += 'exit 20' #everything is fine ssh = ['ssh'] ssh.extend(self.ssh_options + [self.user_host]) ssh.extend(self.config.ssh_prefix_cmd(self.profile_id, cmd_type = list)) ssh.extend([cmd]) logger.debug('Call command: %s' %' '.join(ssh), self) try: subprocess.check_call(ssh, stdout=subprocess.DEVNULL) except subprocess.CalledProcessError as ex: logger.debug('Command returncode: %s' %ex.returncode, self) if ex.returncode == 20: #clean exit pass elif ex.returncode == 11: raise MountException( _('Remote path exists but is not a directory:\n %s') % self.path) elif ex.returncode == 12: raise MountException( _('Remote path is not writeable:\n %s') % self.path) elif ex.returncode == 13: raise MountException( _('Remote path is not executable:\n %s') % self.path) else: raise MountException( _('Couldn\'t create remote path:\n %s') % self.path) else: #returncode is 0 logger.info('Create remote folder %s' %self.path, self)
def check_fuse(self): """ check if sshfs is installed and user is part of group fuse """ logger.debug('Check fuse', self) if not tools.check_command('sshfs'): logger.debug('sshfs is missing', self) raise MountException( _('sshfs not found. Please install e.g. \'apt-get install sshfs\'') ) if self.CHECK_FUSE_GROUP: user = self.config.get_user() try: fuse_grp_members = grp.getgrnam('fuse')[3] except KeyError: #group fuse doesn't exist. So most likely it isn't used by this distribution logger.debug("Group fuse doesn't exist. Skip test", self) return if not user in fuse_grp_members: logger.debug('User %s is not in group fuse' %user, self) raise MountException( _('%(user)s is not member of group \'fuse\'.\n ' 'Run \'sudo adduser %(user)s fuse\'. To apply ' 'changes logout and login again.\nLook at ' '\'man backintime\' for further instructions.') % {'user': user})
def _umount(self): """ Unmount with ``fusermount -u`` for fuse based backends. This **can** be overwritten by backends which subclasses :py:class:`MountControl`. Raises: exceptions.MountException: if unmount failed """ try: subprocess.check_call(['fusermount', '-u', self.currentMountpoint]) except subprocess.CalledProcessError: raise MountException(_('Can\'t unmount %(proc)s from %(mountpoint)s') %{'proc': self.mountproc, 'mountpoint': self.currentMountpoint})
def check_login(self): """ check passwordless authentication to host """ logger.debug('Check login', self) ssh = ['ssh', '-o', 'PreferredAuthentications=publickey'] ssh.extend(self.ssh_options + [self.user_host]) ssh.extend(self.config.ssh_prefix_cmd(self.profile_id, cmd_type = list)) ssh.extend(['echo', '"Hello"']) try: subprocess.check_call(ssh, stdout=subprocess.DEVNULL) except subprocess.CalledProcessError: raise MountException( _('Password-less authentication for %(user)s@%(host)s ' 'failed. Look at \'man backintime\' for further ' 'instructions.') % {'user' : self.user, 'host' : self.host})
def check_known_hosts(self): """ check ssh_known_hosts """ logger.debug('Check known hosts file', self) for host in (self.host, '[%s]:%s' % (self.host, self.port)): proc = subprocess.Popen(['ssh-keygen', '-F', host], stdout=subprocess.PIPE, universal_newlines = True) output = proc.communicate()[0] #subprocess.check_output doesn't exist in Python 2.6 (Debian squeeze default) if output.find('Host %s found' % host) >= 0: logger.debug('Host %s was found in known hosts file' % host, self) return True logger.debug('Host %s is not in known hosts file' %self.host, self) raise MountException( _('%s not found in ssh_known_hosts.') % self.host)
def checkVersion(self): """ check encfs version. 1.7.2 had a bug with --reverse that will create corrupt files """ logger.debug('Check version', self) if self.reverse: proc = subprocess.Popen(['encfs', '--version'], stdout = subprocess.PIPE, stderr = subprocess.STDOUT, universal_newlines = True) output = proc.communicate()[0] m = re.search(r'(\d\.\d\.\d)', output) if m and StrictVersion(m.group(1)) <= StrictVersion('1.7.2'): logger.debug('Wrong encfs version %s' %m.group(1), self) raise MountException(_('encfs version 1.7.2 and before has a bug with option --reverse. Please update encfs'))
def mountprocess_lock_acquire(self, timeout=60): """ block while an other process is mounting or unmounting """ lock_path = self.mount_root lock_suffix = '.lock' lock = os.path.join(lock_path, self.pid + lock_suffix) count = 0 while self.check_locks(lock_path, lock_suffix): count += 1 if count == timeout: raise MountException(_('Mountprocess lock timeout')) sleep(1) logger.debug('Acquire mountprocess lock %s' % lock, self) with open(lock, 'w') as f: f.write(self.pid)
def mounted(self): """ Check if the mountpoint is already mounted. Returns: bool: ``True`` if mountpoint is mounted Raises: exceptions.MountException: if mountpoint is not mounted but also not empty """ if os.path.ismount(self.currentMountpoint): return True else: if os.listdir(self.currentMountpoint): raise MountException(_('mountpoint %s not empty.') % self.currentMountpoint) return False
def check_cipher(self): """ check if both host and localhost support cipher """ if not self.cipher == 'default': logger.debug('Check cipher', self) ssh = ['ssh'] ssh.extend(['-o', 'Ciphers=%s' % self.cipher]) ssh.extend(self.ssh_options + [self.user_host]) ssh.extend(self.config.ssh_prefix_cmd(self.profile_id, cmd_type = list)) ssh.extend(['echo', '"Hello"']) proc = subprocess.Popen(ssh, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, universal_newlines = True) err = proc.communicate()[1] if proc.returncode: logger.debug('Ciper %s is not supported' %self.config.SSH_CIPHERS[self.cipher], self) raise MountException( _('Cipher %(cipher)s failed for %(host)s:\n%(err)s') % {'cipher' : self.config.SSH_CIPHERS[self.cipher], 'host' : self.host, 'err' : err})
def _mount(self): """ mount the service """ sshfs = ['sshfs'] + self.ssh_options if not self.cipher == 'default': sshfs.extend(['-o', 'Ciphers=%s' % self.cipher]) sshfs.extend(['-o', 'idmap=user']) sshfs.extend([self.user_host_path, self.mountpoint]) #bugfix: sshfs doesn't mount if locale in LC_ALL is not available on remote host #LANG or other envirnoment variable are no problem. env = os.environ.copy() if 'LC_ALL' in list(env.keys()): env['LC_ALL'] = 'C' logger.debug('Call mount command: %s' %' '.join(sshfs), self) try: subprocess.check_call(sshfs, env = env) except subprocess.CalledProcessError: raise MountException( _('Can\'t mount %s') % ' '.join(sshfs))
def check_ping_host(self): """ connect to remote port and check if it is open """ logger.debug('Check ping host', self) count = 0 while count < 5: try: with socket.create_connection((self.host, self.port), 2.0) as s: result = s.connect_ex(s.getpeername()) except: result = -1 if result == 0: logger.debug('Host %s is available' %self.host, self) return logger.debug('Could not ping host %s. Try again' %self.host, self) count += 1 sleep(0.2) if result != 0: logger.debug('Failed pinging host %s' %self.host, self) raise MountException( _('Ping %s failed. Host is down or wrong address.') % self.host)
def checkRemoteCommands(self, retry=False): """ Try out all relevant commands used by `Back In Time` on the remote host to make sure snapshots will be successful with the remote host. This will also check that hard-links are supported on the remote host. This check can be disabled with :py:func:`config.Config.sshCheckCommands` Args: retry (bool): retry to run the commands if it failed because the command string was to long Raises: exceptions.MountException: if a command is not supported on remote host or if hard-links are not supported """ if not self.config.sshCheckCommands(): return logger.debug('Check remote commands', self) def maxArg(): if retry: raise MountException( "Checking commands on remote host didn't return any output. " "We already checked the maximum argument lenght but it seem like " "there is an other problem") logger.warning( 'Looks like the command was to long for remote SSHd. We will test max arg length now and retry.', self) import sshMaxArg mid = sshMaxArg.maxArgLength(self.config) sshMaxArg.reportResult(self.host, mid) self.config.setSshMaxArgLength(mid, self.profile_id) return self.checkRemoteCommands(retry=True) remote_tmp_dir_1 = os.path.join(self.path, 'tmp_%s' % self.randomId()) remote_tmp_dir_2 = os.path.join(self.path, 'tmp_%s' % self.randomId()) with tempfile.TemporaryDirectory() as tmp: tmp_file = os.path.join(tmp, 'a') with open(tmp_file, 'wt') as f: f.write('foo') #check rsync rsync1 = tools.rsyncPrefix(self.config, no_perms=False, progress=False) rsync1.append(tmp_file) rsync1.append('%s@%s:"%s"/' % (self.user, tools.escapeIPv6Address( self.host), remote_tmp_dir_1)) #check remote rsync hard-link support rsync2 = tools.rsyncPrefix(self.config, no_perms=False, progress=False) rsync2.append('--link-dest=../%s' % os.path.basename(remote_tmp_dir_1)) rsync2.append(tmp_file) rsync2.append('%s@%s:"%s"/' % (self.user, tools.escapeIPv6Address( self.host), remote_tmp_dir_2)) for cmd in (rsync1, rsync2): logger.debug('Check rsync command: %s' % cmd, self) proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) out, err = proc.communicate() if err or proc.returncode: logger.debug('rsync command returned error: %s' % err, self) raise MountException( _('Remote host %(host)s doesn\'t support \'%(command)s\':\n' '%(err)s\nLook at \'man backintime\' for further instructions' ) % { 'host': self.host, 'command': cmd, 'err': err }) #check cp chmod find and rm head = 'tmp1="%s"; tmp2="%s"; ' % (remote_tmp_dir_1, remote_tmp_dir_2) #first define a function to clean up and exit head += 'cleanup(){ ' head += 'test -e "$tmp1/a" && rm "$tmp1/a" >/dev/null 2>&1; ' head += 'test -e "$tmp2/a" && rm "$tmp2/a" >/dev/null 2>&1; ' head += 'test -e smr.lock && rm smr.lock >/dev/null 2>&1; ' head += 'test -e "$tmp1" && rmdir "$tmp1" >/dev/null 2>&1; ' head += 'test -e "$tmp2" && rmdir "$tmp2" >/dev/null 2>&1; ' head += 'test -n "$tmp3" && test -e "$tmp3" && rmdir "$tmp3" >/dev/null 2>&1; ' head += 'exit $1; }; ' tail = [] #list inodes cmd = 'ls -i "$tmp1/a"; ls -i "$tmp2/a"; ' tail.append(cmd) #try nice -n 19 if self.nice: cmd = 'echo \"nice -n 19\"; nice -n 19 true >/dev/null; err_nice=$?; ' cmd += 'test $err_nice -ne 0 && cleanup $err_nice; ' tail.append(cmd) #try ionice -c2 -n7 if self.ionice: cmd = 'echo \"ionice -c2 -n7\"; ionice -c2 -n7 true >/dev/null; err_nice=$?; ' cmd += 'test $err_nice -ne 0 && cleanup $err_nice; ' tail.append(cmd) #try nocache if self.nocache: cmd = 'echo \"nocache\"; nocache true >/dev/null; err_nocache=$?; ' cmd += 'test $err_nocache -ne 0 && cleanup $err_nocache; ' tail.append(cmd) #try screen, bash and flock used by smart-remove running in background if self.config.smartRemoveRunRemoteInBackground(self.profile_id): cmd = 'echo \"screen -d -m bash -c ...\"; screen -d -m bash -c \"true\" >/dev/null; err_screen=$?; ' cmd += 'test $err_screen -ne 0 && cleanup $err_screen; ' tail.append(cmd) cmd = 'echo \"(flock -x 9) 9>smr.lock\"; bash -c \"(flock -x 9) 9>smr.lock\" >/dev/null; err_flock=$?; ' cmd += 'test $err_flock -ne 0 && cleanup $err_flock; ' tail.append(cmd) cmd = 'echo \"rmdir \$(mktemp -d)\"; tmp3=$(mktemp -d); test -z "$tmp3" && cleanup 1; rmdir $tmp3 >/dev/null; err_rmdir=$?; ' cmd += 'test $err_rmdir -ne 0 && cleanup $err_rmdir; ' tail.append(cmd) #if we end up here, everything should be fine cmd = 'echo \"done\"; cleanup 0' tail.append(cmd) maxLength = self.config.sshMaxArgLength(self.profile_id) additionalChars = len('echo ""') + len( self.config.sshPrefixCmd(self.profile_id, cmd_type=str)) output = '' err = '' returncode = 0 for cmd in tools.splitCommands(tail, head=head, maxLength=maxLength - additionalChars): if cmd.endswith('; '): cmd += 'echo ""' c = self.config.sshCommand( cmd=[cmd], custom_args=['-p', str(self.port), self.user_host], port=False, user_host=False, nice=False, ionice=False, profile_id=self.profile_id) try: logger.debug('Call command: %s' % ' '.join(c), self) proc = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) ret = proc.communicate() except OSError as e: #Argument list too long if e.errno == 7: logger.debug('Argument list too log (Python exception)', self) return maxArg() else: raise logger.debug('Command stdout: %s' % ret[0], self) logger.debug('Command stderr: %s' % ret[1], self) logger.debug('Command returncode: %s' % proc.returncode, self) output += ret[0].strip('\n') + '\n' err += ret[1].strip('\n') + '\n' returncode += proc.returncode if proc.returncode: break output_split = output.strip('\n').split('\n') while True: if output_split and not output_split[-1]: output_split = output_split[:-1] else: break if not output_split: return maxArg() if returncode or not output_split[-1].startswith('done'): for command in ('rm', 'nice', 'ionice', 'nocache', 'screen', '(flock'): if output_split[-1].startswith(command): raise MountException( _('Remote host %(host)s doesn\'t support \'%(command)s\':\n' '%(err)s\nLook at \'man backintime\' for further instructions' ) % { 'host': self.host, 'command': output_split[-1], 'err': err }) raise MountException( _('Check commands on host %(host)s returned unknown error:\n' '%(err)s\nLook at \'man backintime\' for further instructions' ) % { 'host': self.host, 'err': err }) inodes = [] for tmp in (remote_tmp_dir_1, remote_tmp_dir_2): for line in output_split: m = re.match(r'^(\d+).*?%s' % tmp, line) if m: inodes.append(m.group(1)) logger.debug('remote inodes: ' + ' | '.join(inodes), self) if len(inodes) == 2 and inodes[0] != inodes[1]: raise MountException( _('Remote host %s doesn\'t support hardlinks') % self.host)
def unlockSshAgent(self, force=False): """ Unlock the private key in ``ssh-agent`` which will provide it for all other commands. The password to unlock the key will be provided by ``backintime-askpass``. Args: force (bool): force to unlock the key by removing it first and add it again to make sure, the given values are correct Raises: exceptions.MountException: if unlock failed """ self.startSshAgent() env = os.environ.copy() env['SSH_ASKPASS'] = '******' env['ASKPASS_PROFILE_ID'] = self.profile_id env['ASKPASS_MODE'] = self.mode if force: #remove private key first so we can check if the given password is valid logger.debug( 'Remove private key %s from ssh agent' % self.private_key_file, self) proc = subprocess.Popen(['ssh-add', '-d', self.private_key_file], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, universal_newlines=True) proc.communicate() proc = subprocess.Popen(['ssh-add', '-l'], stdout=subprocess.PIPE, universal_newlines=True) output = proc.communicate()[0] if force or not output.find(self.private_key_fingerprint) >= 0: logger.debug( 'Add private key %s to ssh agent' % self.private_key_file, self) password_available = any([ self.config.passwordSave(self.profile_id), self.config.passwordUseCache(self.profile_id), not self.password is None ]) logger.debug('Password available: %s' % password_available, self) if not password_available and not tools.checkXServer(): #we need to unlink stdin from ssh-add in order to make it #use our own backintime-askpass. #But because of this we can NOT use getpass inside backintime-askpass #if password is not saved and there is no x-server. #So, let's just keep ssh-add asking for the password in that case. alarm = tools.Alarm() alarm.start(10) try: proc = subprocess.call(['ssh-add', self.private_key_file]) alarm.stop() except tools.Timeout: pass else: if self.password: logger.debug('Provide password through temp FIFO', self) thread = password_ipc.TempPasswordThread(self.password) env['ASKPASS_TEMP'] = thread.temp_file thread.start() proc = subprocess.Popen(['ssh-add', self.private_key_file], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, preexec_fn=os.setsid, universal_newlines=True) output, error = proc.communicate() if proc.returncode: logger.error( 'Failed to unlock SSH private key %s: %s' % (self.private_key_file, error), self) if self.password: thread.stop() proc = subprocess.Popen(['ssh-add', '-l'], stdout=subprocess.PIPE, universal_newlines=True) output = proc.communicate()[0] if not output.find(self.private_key_fingerprint) >= 0: logger.debug( 'Was not able to unlock private key %s' % self.private_key_file, self) raise MountException( _('Could not unlock ssh private key. Wrong password ' 'or password not available for cron.')) else: logger.debug( 'Private key %s is already unlocked in ssh agent' % self.private_key_file, self)
def test_mount_exception(self, takeSnapshot, mount, sleep): mount.side_effect = MountException() takeSnapshot.return_value = [True, False] self.assertFalse(self.sn.backup()) self.assertFalse(takeSnapshot.called)
def test_umount_exception(self, takeSnapshot, umount, sleep): umount.side_effect = MountException() takeSnapshot.return_value = [True, False] self.assertTrue(self.sn.backup())
def check_remote_commands(self, retry = False): """ try all relevant commands for take_snapshot on remote host. specialy embedded Linux devices using 'BusyBox' sometimes doesn't support everything that is need to run backintime. also check for hardlink-support on remote host. """ logger.debug('Check remote commands', self) def maxArg(): if retry: raise MountException("Checking commands on remote host didn't return any output. " "We already checked the maximum argument lenght but it seem like " "there is an other problem") logger.warning('Looks like the command was to long for remote SSHd. We will test max arg length now and retry.', self) import sshMaxArg mid = sshMaxArg.test_ssh_max_arg(self.user_host) sshMaxArg.reportResult(self.host, mid) self.config.set_ssh_max_arg_length(mid, self.profile_id) return self.check_remote_commands(retry = True) #check rsync tmp_file = tempfile.mkstemp()[1] rsync = tools.get_rsync_prefix( self.config ) + ' --dry-run --chmod=Du+wx %s ' % tmp_file rsync += '"%s@%s:%s"' % (self.user, self.host, self.path) logger.debug('Check rsync command: %s' %rsync, self) #use os.system for compatiblity with snapshots.py err = os.system(rsync) if err: logger.debug('Rsync command returnd error: %s' %err, self) os.remove(tmp_file) raise MountException( _('Remote host %(host)s doesn\'t support \'%(command)s\':\n' '%(err)s\nLook at \'man backintime\' for further instructions') % {'host' : self.host, 'command' : rsync, 'err' : err}) os.remove(tmp_file) #check cp chmod find and rm remote_tmp_dir = os.path.join(self.path, 'tmp_%s' % self.random_id()) head = 'tmp=%s ; ' % remote_tmp_dir #first define a function to clean up and exit head += 'cleanup(){ ' head += 'test -e $tmp/a && rm $tmp/a >/dev/null 2>&1; ' head += 'test -e $tmp/b && rm $tmp/b >/dev/null 2>&1; ' head += 'test -e smr.lock && rm smr.lock >/dev/null 2>&1; ' head += 'test -e $tmp && rmdir $tmp >/dev/null 2>&1; ' head += 'exit $1; }; ' tail = [] #create tmp_RANDOM dir and file a cmd = 'test -e $tmp || mkdir $tmp; touch $tmp/a; ' tail.append(cmd) #try to create hardlink b from a cmd = 'echo \"cp -aRl SOURCE DEST\"; cp -aRl $tmp/a $tmp/b >/dev/null; err_cp=$?; ' cmd += 'test $err_cp -ne 0 && cleanup $err_cp; ' tail.append(cmd) #list inodes of a and b cmd = 'ls -i $tmp/a; ls -i $tmp/b; ' tail.append(cmd) #try to chmod cmd = 'echo \"chmod u+rw FILE\"; chmod u+rw $tmp/a >/dev/null; err_chmod=$?; ' cmd += 'test $err_chmod -ne 0 && cleanup $err_chmod; ' tail.append(cmd) #try to find and chmod cmd = 'echo \"find PATH -type f -exec chmod u-wx \"{}\" \\;\"; ' cmd += 'find $tmp -type f -exec chmod u-wx \"{}\" \\; >/dev/null; err_find=$?; ' cmd += 'test $err_find -ne 0 && cleanup $err_find; ' tail.append(cmd) #try find suffix '+' cmd = 'find $tmp -type f -exec chmod u-wx \"{}\" + >/dev/null; err_gnu_find=$?; ' cmd += 'test $err_gnu_find -ne 0 && echo \"gnu_find not supported\"; ' tail.append(cmd) #try to rm -rf cmd = 'echo \"rm -rf PATH\"; rm -rf $tmp >/dev/null; err_rm=$?; ' cmd += 'test $err_rm -ne 0 && cleanup $err_rm; ' tail.append(cmd) #try nice -n 19 if self.nice: cmd = 'echo \"nice -n 19\"; nice -n 19 true >/dev/null; err_nice=$?; ' cmd += 'test $err_nice -ne 0 && cleanup $err_nice; ' tail.append(cmd) #try ionice -c2 -n7 if self.ionice: cmd = 'echo \"ionice -c2 -n7\"; ionice -c2 -n7 true >/dev/null; err_nice=$?; ' cmd += 'test $err_nice -ne 0 && cleanup $err_nice; ' tail.append(cmd) #try nocache if self.nocache: cmd = 'echo \"nocache\"; nocache true >/dev/null; err_nocache=$?; ' cmd += 'test $err_nocache -ne 0 && cleanup $err_nocache; ' tail.append(cmd) #try screen, bash and flock used by smart-remove running in background if self.config.get_smart_remove_run_remote_in_background(self.profile_id): cmd = 'echo \"screen -d -m bash -c ...\"; screen -d -m bash -c \"true\" >/dev/null; err_screen=$?; ' cmd += 'test $err_screen -ne 0 && cleanup $err_screen; ' cmd += 'echo \"(flock -x 9) 9>smr.lock\"; bash -c \"(flock -x 9) 9>smr.lock\" >/dev/null; err_flock=$?; ' cmd += 'test $err_flock -ne 0 && cleanup $err_flock; ' tail.append(cmd) #if we end up here, everything should be fine cmd = 'echo \"done\"' tail.append(cmd) maxLength = self.config.ssh_max_arg_length(self.profile_id) additionalChars = len('echo ""') + len(self.config.ssh_prefix_cmd(self.profile_id, cmd_type = str)) ssh = ['ssh'] ssh.extend(self.ssh_options + [self.user_host]) ssh.extend(self.config.ssh_prefix_cmd(self.profile_id, cmd_type = list)) output = '' err = '' returncode = 0 for cmd in tools.splitCommands(tail, head = head, maxLength = maxLength, additionalChars = additionalChars): if cmd.endswith('; '): cmd += 'echo ""' c = ssh[:] c.extend([cmd]) try: logger.debug('Call command: %s' %' '.join(c), self) proc = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines = True) ret = proc.communicate() except OSError as e: #Argument list too long if e.errno == 7: logger.debug('Argument list too log (Python exception)', self) return maxArg() else: raise logger.debug('Command stdout: %s' %ret[0], self) logger.debug('Command stderr: %s' %ret[1], self) logger.debug('Command returncode: %s' %proc.returncode, self) output += ret[0].strip('\n') + '\n' err += ret[1].strip('\n') + '\n' returncode += proc.returncode if proc.returncode: break output_split = output.strip('\n').split('\n') while True: if output_split and not output_split[-1]: output_split = output_split[:-1] else: break if not output_split: return maxArg() gnu_find_suffix_support = True for line in output_split: if line.startswith('gnu_find not supported'): gnu_find_suffix_support = False self.config.set_gnu_find_suffix_support(gnu_find_suffix_support, self.profile_id) if returncode or not output_split[-1].startswith('done'): for command in ('cp', 'chmod', 'find', 'rm', 'nice', 'ionice', 'nocache', 'screen', '(flock'): if output_split[-1].startswith(command): raise MountException( _('Remote host %(host)s doesn\'t support \'%(command)s\':\n' '%(err)s\nLook at \'man backintime\' for further instructions') % {'host' : self.host, 'command' : output_split[-1], 'err' : err}) raise MountException( _('Check commands on host %(host)s returned unknown error:\n' '%(err)s\nLook at \'man backintime\' for further instructions') % {'host' : self.host, 'err' : err}) i = 1 inode1 = 'ABC' inode2 = 'DEF' for line in output_split: if line.startswith('cp'): try: inode1 = output_split[i].split(' ')[0] inode2 = output_split[i+1].split(' ')[0] except IndexError: pass if not inode1 == inode2: raise MountException( _('Remote host %s doesn\'t support hardlinks') % self.host) i += 1