diff --git a/backup_check b/backup_check index a52e68e..7ae0823 100755 --- a/backup_check +++ b/backup_check @@ -1,15 +1,8 @@ #!/usr/bin/env python3 import argparse import os -import sys -import re -import logging -import paramiko import datetime -import glob -import shutil import time -import difflib def get_args(): @@ -64,348 +57,6 @@ def main(): if args.verbose: print(f'{args.host}: last backup {backuptimestamp}, {agetext} ago') - - -#class Config: -# def __init__(self, workdir): -# self.workdir = workdir -# self.ssh = None -# self.host = None -# self.user = None -# self.passwd = None -# self.sshkey = None -# self.config = None -# self.extra = {} -# self.oldconfig = None -# -# def connect(self, host=None, user=None, passwd=None, sshkey=None): -# if host: self.host = host -# if user: self.user = user -# if passwd: self.passwd = passwd -# if sshkey: self.sshkey = sshkey -# if not self.host or not self.user: -# raise ValueError('internal error: missing host or user') -# if not self.passwd and not self.sshkey: -# raise ValueError('no password and no ssh key given') -# -# 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) -# -# def prepare_config_for_diff(self, configlines): -# return configlines -# -# -# def has_changed(self, fn, diffprint=False, difflog=False): -# try: -# with open(f'{self.workdir}/{fn}.config', 'r') as f: -# self.oldconfig = f.read() -# except: -# log.info('No previous configuration found.') -# return True -# -# if self.oldconfig == self.config: -# return False -# -# oldtimestamp = 'previous' -# try: -# with open(f'{self.workdir}/{fn}.lastchange', 'r') as f: -# oldunixts = int(f.readline()) -# oldtimestamp = datetime.datetime.fromtimestamp(oldunixts) -# except: -# pass -# -# contents = {} -# for cf, n in ((self.oldconfig, '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' -# ): -# has_changed = True -# if diffprint: -# print(line.rstrip()) -# if difflog: -# log.info(line.rstrip()) -# return has_changed -# -# def write_to_disk(self, fn, has_changed=True): -# basepath = f'{self.workdir}/{fn}' -# with open(f'{basepath}.config', 'w') as f: -# f.write(self.config) -# for e in self.extra: -# if self.extra[e]['type'] == 'binary': -# mode = 'wb' -# else: -# mode = 'w' -# with open(f'{basepath}.{e}', mode) as f: -# f.write(self.extra[e]['content']) -# 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}.*') -# -# def backup_old(self, fn): -# basepath = f'{self.workdir}/{fn}' -# try: -# info = os.stat(basepath +'.config') -# oldtimestamp = datetime.datetime.fromtimestamp(info.st_mtime).strftime('%F_%H%M%S') -# except FileNotFoundError: -# return -# -# backuppath = f'{basepath}+{oldtimestamp}' -# os.mkdir(backuppath) -# for f in glob.glob(f'{basepath}.*'): -# shutil.move(f, backuppath) -# log.info(f'Moved old backup into: {backuppath}') -# -# -# -#class RouterosConfig(Config): -# def __init__(self, workdir): -# super().__init__(workdir) -# self.extra['resource'] = { -# 'type': 'text', -# 'content': None -# } -# self.extra['iproute'] = { -# 'type': 'text', -# 'content': None -# } -# self.extra['backup'] = { -# 'type': 'binary', -# 'content': None -# } -# -# def connect(self, host=None, user=None, passwd=None, key=None): -# if user: -# user += '+c' -# super().connect(host, user, passwd, key) -# -# def get_config(self): -# stdin, stdout, stderr = self.ssh.exec_command('/system resource print') -# resource = [] -# ros_version = None -# for line in stdout: -# line = line.strip() -# resource.append(line) -# if m:= re.match('version: *(\d)', line): -# ros_version = int(m[1]) -# if ros_version == 6: -# export_cmds = [ -# '/export terse', -# '/user export terse' -# ] -# certificate_cmd = '/certificate print detail' -# -# elif ros_version == 7: -# export_cmds = [ -# '/export terse show-sensitive', -# '/user/export terse show-sensitive' -# ] -# certificate_cmd = '/certificate/print show-ids detail' -# else: -# raise RuntimeError("routeros version not found in system resource") -# -# export = [] -# for cmd in export_cmds: -# stdin, stdout, stderr = self.ssh.exec_command(cmd) -# export.append('# '+ cmd.center(76, '=')) -# preamble = True -# append_to_last = False -# for line in stdout: -# line = line.rstrip() -# if preamble: -# if re.match('# .* by RouterOS ', line): -# continue -# if line[0] != '#': -# preamble = False -# export.append(line) -# continue -# if re.match('# .* not ', line): -# append_to_last = True -# continue -# if append_to_last: -# export[-1] += ' ' + line -# append_to_last = False -# else: -# export.append(line) -# -# stdin, stdout, stderr = self.ssh.exec_command(certificate_cmd) -# export.append('# '+ certificate_cmd.center(76, '=')) -# for line in stdout: -# line = line.rstrip() -# line = re.sub(' *(days-valid|expires-after)=[^ ]*', '', line) -# export.append(line) -# -# iproute = [] -# for cmd in ('/ip address print', '/ip route print'): -# stdin, stdout, stderr = self.ssh.exec_command(cmd) -# iproute.append('# '+ cmd.center(76, '=')) -# for line in stdout: -# line = line.rstrip() -# iproute.append(line) -# -# stdin, stdout, stderr = self.ssh.exec_command('/system backup save dont-encrypt=yes name=autobck') -# if not re.search('Configuration backup saved', str(stdout.read(), 'utf8')): -# raise RuntimeError('"/system backup" failed') -# -# sftp = self.ssh.open_sftp() -# with sftp.open('autobck.backup', 'r') as sf: -# backup = sf.read() -# -# self.config = '\n'.join(export) -# self.extra['resource']['content'] = '\n'.join(resource) -# self.extra['iproute']['content'] = '\n'.join(iproute) -# self.extra['backup']['content'] = backup -# -# def prepare_config_for_diff(self, configlines): -# prepared = [] -# for line in configlines: -# if re.match('/ip[ /]ipsec[ /]policy[ /]add', line): -# line = re.sub('(sa-(src|dst)-address)=[^ ]*', '\\1=...', line) -# prepared.append(line) -# return prepared -# -# -#class IosConfig(Config): -# def __init__(self, workdir): -# super().__init__(workdir) -# self.extra['iproute'] = { -# 'type': 'text', -# 'content': None -# } -# -# def get_config(self): -# shrun = [] -# stdin, stdout, stderr = self.ssh.exec_command('sh run') -# for line in stdout: -# line = line.rstrip() -# shrun.append(line) -# -# iproute = [] -# for cmd in ('sh ip aliases', 'sh ip route'): -# # IOS allows only 1 exec per connection: -# self.connect() -# stdin, stdout, stderr = self.ssh.exec_command(cmd) -# iproute.append('# '+ cmd.center(76, '=')) -# for line in stdout: -# line = line.rstrip() -# iproute.append(line) -# -# self.config = '\n'.join(shrun) -# self.extra['iproute']['content'] = '\n'.join(iproute) -# -# def prepare_config_for_diff(self, configlines): -# prepared = [] -# for line in configlines: -# if not re.search('\S', line): -# continue -# if re.match('! Last configuration change at ', line): -# continue -# prepared.append(line) -# return prepared -# -# -#class AsaConfig(Config): -# def __init__(self, workdir): -# super().__init__(workdir) -# self.extra['iproute'] = { -# 'type': 'text', -# 'content': None -# } -# -# def asa_command(self, channel, cmd): -# start = time.time() -# while not channel.send_ready(): -# if time.time() - start > 10: -# raise RuntimeError('ASA timeout') -# time.sleep(0.05) -# channel.send(cmd +'\n') -# time.sleep(1) -# while not channel.recv_ready(): -# if time.time() - start > 30: -# raise RuntimeError('ASA timeout') -# time.sleep(0.05) -# output = '' -# while channel.recv_ready(): -# output += str(channel.recv(65536), 'utf8') -# return output.splitlines() -# -# def get_config(self): -# shrun = [] -# channel = self.ssh.invoke_shell() -# self.asa_command(channel, 'term pager 0') -# shrun = self.asa_command(channel, 'sh run') -# -# iproute = [] -# for cmd in ('sh ip address', 'sh route'): -# iproute.append('# '+ cmd.center(76, '=')) -# output = self.asa_command(channel, cmd) -# iproute += output -# -# self.config = '\n'.join(shrun) -# self.extra['iproute']['content'] = '\n'.join(iproute) -# -# -#def setup_logging(hostname, logfile, loglevel): -# handler = logging.FileHandler(logfile, 'a') -# fmt = logging.Formatter( -# "{asctime} "+ hostname +"[{process}] {name}|{levelname} {message}", -# style='{') -# handler.setFormatter(fmt) -# level = logging.getLevelName(loglevel) -# if type(level) != int: -# level = logging.INFO -# logging.basicConfig(handlers=[handler], level=level) -# if level > logging.DEBUG: -# # paramiko is too verbose -# logging.getLogger('paramiko').setLevel(logging.WARNING) -# global log -# log = logging.getLogger('main') -# -# -#def main(): -# args = get_args() -# if args.logfile: -# setup_logging(args.host, args.logfile, args.loglevel) -# try: -# if args.type not in ('routeros', 'ios', 'asa'): -# raise ValueError(f'Invalid devtype: {args.type}') -# -# if args.type == 'routeros': -# cf = RouterosConfig(workdir=args.outdir) -# elif args.type == 'ios': -# cf = IosConfig(workdir=args.outdir) -# elif args.type == 'asa': -# cf = AsaConfig(workdir=args.outdir) -# -# address = args.address or args.host -# log.info(f'Connecting to {args.type}: {args.user}@{address}') -# start = time.time() -# cf.connect(address, args.user, args.password, args.sshkey) -# cf.get_config() -# -# filename = args.host -# log.debug(f'Backup filename base: {filename}') -# -# has_changed = cf.has_changed(filename, diffprint=args.diffprint, difflog=args.difflog) -# if has_changed: -# cf.backup_old(filename) -# # 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) -# log.info(f'Finished in {time.time()-start:.1f}s.') -# except KeyboardInterrupt: -# log.error(f'Killed by KeyboardInterrupt.') -# except Exception as e: -# log.exception(f'Exception: %s', e) -# if not args.noerrors: -# print(f'Error: {e}') - if __name__ == "__main__": main() # vim: set ft=python tabstop=4 shiftwidth=4 expandtab smarttab: diff --git a/backup_diff b/backup_diff new file mode 100755 index 0000000..ef6b3e0 --- /dev/null +++ b/backup_diff @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +import argparse +import os +import re +import glob +import difflib + + +def get_args(): + parser = argparse.ArgumentParser( + description='backup_diff - show differences') + + parser.add_argument('-H', '--host', + required=True, + action='store', + help='host name, used also as backup filename base ') + parser.add_argument('-o', '--backupdir', + required=True, + action='store', + help='backup directory') + parser.add_argument('nums', + nargs='*', + action='store', + type=int, + help='create diff between versions') + + args = parser.parse_args() + return args + +def find_backups(backupdir, host): + basepath = f'{backupdir}/{host}' + backups = [] + for fn in sorted(glob.glob(f'{basepath}+*/*.lastchange')): + if m := re.search('\+(\d{4}-\d{2}-\d{2})_(\d\d)(\d\d)(\d\d)', fn): + moved = f'{m[1]} {m[2]}:{m[3]}:{m[4]}' + else: + moved = '(undef)' + with open(fn, 'r') as f: + lastchange = f.readlines()[-1].rstrip() + backups.insert(0, { + 'since': lastchange, + 'until': moved, + 'cfn': re.sub('\.lastchange', '.config', fn) + }) + try: + with open(f'{basepath}.lastchange', 'r') as f: + lastchange = f.readlines()[-1].rstrip() + backups.insert(0, { + 'since': lastchange, + 'until': 'now', + 'cfn': f'{basepath}.config' + }) + except: + pass + return backups + +def diff(backups, host, n1, n2): + contents = [] + name = [] + for i in (n1, n2): + if i >= len(backups): + i = len(backups) - 1 + if i < 0: + i = 0 + with open(backups[i]['cfn']) as f: + contents.append(f.readlines()) + name.append(f"[{i:3}] {host} | {backups[i]['since']} - {backups[i]['until']}") + + differ = False + for line in difflib.unified_diff( + contents[0], contents[1], + fromfile=name[0], tofile=name[1] + ): + print(line.rstrip()) + differ = True + if not differ: + print(f'--- {name[0]}') + print(f'+++ {name[1]}') + print('no difference') + +def main(): + args = get_args() + backups = find_backups(args.backupdir, args.host) + if not len(backups): + print(f'No backups found for host: {args.host}') + exit(1) + + if len(args.nums) == 1: + diff(backups, args.host, args.nums[0], 0) + elif len(args.nums) > 1: + diff(backups, args.host, args.nums[0], args.nums[1]) + else: + for i in range(len(backups)-1, -1, -1): + print(f"{i:3}: {backups[i]['since']} - {backups[i]['until']}") + + +if __name__ == "__main__": + main() +# vim: set ft=python tabstop=4 shiftwidth=4 expandtab smarttab: