This commit is contained in:
ROTTLER Tamas 2023-02-28 03:00:13 +01:00
parent 5557063b78
commit a9492bfbe7
2 changed files with 99 additions and 349 deletions

View File

@ -1,15 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse import argparse
import os import os
import sys
import re
import logging
import paramiko
import datetime import datetime
import glob
import shutil
import time import time
import difflib
def get_args(): def get_args():
@ -64,348 +57,6 @@ def main():
if args.verbose: if args.verbose:
print(f'{args.host}: last backup {backuptimestamp}, {agetext} ago') 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__": if __name__ == "__main__":
main() main()
# vim: set ft=python tabstop=4 shiftwidth=4 expandtab smarttab: # vim: set ft=python tabstop=4 shiftwidth=4 expandtab smarttab:

99
backup_diff Executable file
View File

@ -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: