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

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: