diff --git a/routerbackup b/routerbackup index 5afea6e..5094d0d 100755 --- a/routerbackup +++ b/routerbackup @@ -78,7 +78,10 @@ class Config: self.sshkey = None self.config = None self.extra = {} - self.oldconfig = None + self.filebase = None + self.prev_config = None + self.prev_timestamp = None + self.prev_diff = [] def connect(self, host=None, user=None, passwd=None, sshkey=None): if host: self.host = host @@ -92,8 +95,14 @@ class Config: self.ssh = paramiko.SSHClient() self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy) - self.ssh.connect(self.host, username=self.user, - password=self.passwd, key_filename=self.sshkey) + try: + self.ssh.connect(self.host, username=self.user, + password=self.passwd, key_filename=self.sshkey) + except: + # disabling sha2 makes sha1 default for older devices (routeros 6) + self.ssh.connect(self.host, username=self.user, + password=self.passwd, key_filename=self.sshkey, + disabled_algorithms = dict(pubkeys=['rsa-sha2-256', 'rsa-sha2-512'])) def is_bad_config(self): if not self.config: @@ -105,44 +114,58 @@ class Config: def _prepare_config_for_diff(self, configlines): return configlines + def set_filebase(self, base): + self.filebase = base - def has_changed(self, fn, diffprint=False, difflog=False): + def load_previous(self): try: - with open(f'{self.workdir}/{fn}.config', 'r') as f: - self.oldconfig = f.read() + with open(f'{self.workdir}/{self.filebase}.config', 'r') as f: + self.prev_config = f.read() except: log.info('No previous configuration found.') - return True + self.prev_config = None + return - if self.oldconfig == self.config: - return False - - oldtimestamp = 'previous' + self.prev_timestamp = 'previous' try: - with open(f'{self.workdir}/{fn}.lastchange', 'r') as f: + with open(f'{self.workdir}/{self.filebase}.lastchange', 'r') as f: oldunixts = int(f.readline()) - oldtimestamp = datetime.datetime.fromtimestamp(oldunixts) + self.prev_timestamp = datetime.datetime.fromtimestamp(oldunixts) except: pass + def has_previous(self): + return True if self.prev_config else False + + def has_changed(self): + self.prev_diff = [] + if not self.prev_config: + return True + if self.prev_config == self.config: + return False + contents = {} - for cf, n in ((self.oldconfig, 'old'), (self.config, 'new')): + for cf, n in ((self.prev_config, 'old'), (self.config, 'new')): contents[n] = self._prepare_config_for_diff(cf.splitlines(keepends=True)) has_changed = False for line in difflib.unified_diff( contents['old'], contents['new'], - fromfile=f'{fn} {oldtimestamp}', tofile=f'{fn} now' + fromfile=f'{self.filebase} {self.prev_timestamp}', tofile=f'{self.filebase} now' ): has_changed = True - if diffprint: - print(line.rstrip()) - if difflog: - log.info(line.rstrip()) + self.prev_diff.append(line.rstrip()) return has_changed - def write_to_disk(self, fn, has_changed=True): - basepath = f'{self.workdir}/{fn}' + def print_changed(self, diffprint=False, difflog=False): + for line in self.prev_diff: + if diffprint: + print(line) + if difflog: + log.info(line) + + def write_to_disk(self, has_changed=True): + basepath = f'{self.workdir}/{self.filebase}' with open(f'{basepath}.config', 'w') as f: f.write(self.config) for e in self.extra: @@ -155,10 +178,10 @@ class Config: if has_changed: with open(f'{basepath}.lastchange', 'w') as f: f.write(f'{int(time.time())}\n{time.strftime("%F %T")}\n') - log.info(f'Written to: {basepath}.*') + log.info(f'Written {"differing" if has_changed else "similar"} configuration to: {basepath}.*') - def backup_old(self, fn): - basepath = f'{self.workdir}/{fn}' + def backup_old(self): + basepath = f'{self.workdir}/{self.filebase}' try: info = os.stat(basepath +'.config') oldtimestamp = datetime.datetime.fromtimestamp(info.st_mtime).strftime('%F_%H%M%S') @@ -337,7 +360,7 @@ class ConfigWithShell(Config): while not channel.send_ready(): if time.time() - start > send_timeout: raise RuntimeError('shell send timeout') - time.sleep(0.05) + time.sleep(0.1) channel.send(cmd +'\n') start = time.time() @@ -351,7 +374,7 @@ class ConfigWithShell(Config): return output.splitlines() if time.time() - start > recv_timeout: raise RuntimeError(f'shell recv timeout (after receiving {len(output)} characters)') - time.sleep(0.05) + time.sleep(0.1) while channel.recv_ready(): output += str(channel.recv(65536), 'utf8') @@ -450,25 +473,47 @@ def main(): elif args.type == 'comware': cf = ComwareConfig(workdir=args.backupdir) + filebase = args.host + cf.set_filebase(filebase) + log.debug(f'Backup filename base: {filebase}') + cf.load_previous() + address = args.address or args.host log.info(f'Connecting to {args.type}: {args.user}@{address} with{" password" if args.password else""}{" ssh-key" if args.sshkey else""}') start = time.time() os.environ['HOME'] = '/' # prevent paramiko from searching private keys - cf.connect(address, args.user, args.password, args.sshkey or '') - cf.get_config() - if reason := cf.is_bad_config(): - log.warning(f'Bad configuration: {reason}') - return + has_changed = False + changed_count = 0 + for i in range(3): + cf.connect(address, args.user, args.password, args.sshkey or '') + cf.get_config() - filename = args.host - log.debug(f'Backup filename base: {filename}') + if reason := cf.is_bad_config(): + log.warning(f'Bad configuration: {reason}') + #return + time.sleep(1) + continue + + has_changed = cf.has_changed() + if not has_changed: + # first try: good + # second try: first was probably incomplete -> good + break + if not cf.has_previous(): + break + changed_count += 1 + log.info(f'Got differing configuration on {changed_count}. attempt.') + if changed_count >= 2: + # got changed config twice, that is no mistake + break + time.sleep(3) - has_changed = cf.has_changed(filename, diffprint=args.diffprint, difflog=args.difflog) if has_changed: - cf.backup_old(filename) + cf.print_changed(diffprint=args.diffprint, difflog=args.difflog) + cf.backup_old() # overwrite unchanged config, too, to update modification time and # also store changes that get skipped by _prepare_config_for_diff(): - cf.write_to_disk(filename, has_changed) + cf.write_to_disk(has_changed) log.info(f'Finished in {time.time()-start:.1f}s.') except KeyboardInterrupt: log.error(f'Killed by KeyboardInterrupt.')