commit 5249fe0d10399d58d4ee12332124c58711076cd6 Author: Rottler Tamas Date: Mon Feb 27 18:15:12 2023 +0100 1. diff --git a/ansible-role-routerbackup/tasks/backup.yml b/ansible-role-routerbackup/tasks/backup.yml new file mode 100644 index 0000000..0385448 --- /dev/null +++ b/ansible-role-routerbackup/tasks/backup.yml @@ -0,0 +1,41 @@ +--- +- name: set device type + set_fact: + devtype: > + {% if 'routeros' in group_names %}routeros + {% elif 'ciscoasa' in group_names %}asa + {% elif 'ciscoios' in group_names %}ios + {% else %}undefined + {% endif %} + +- name: backup + ansible.builtin.command: + cmd: > + {{ routerbackup_bin }}/routerbackup --difflog --diffprint --logfile {{ routerbackup_log }} + --backupdir {{ routerbackup_dir }} --type {{ devtype }} + --host {{ inventory_hostname }} --address {{ ansible_host }} --user {{ ansible_user }} + {% if ansible_private_key_file is defined %}-i {{ ansible_private_key_file }}{% endif %} + {% if ansible_ssh_pass is defined %}-p {{ ansible_ssh_pass }}{% endif %} + delegate_to: localhost + changed_when: false + register: diff + +- name: create empty diff file + copy: + content: "" + dest: "{{ routerbackup_diff }}" + delegate_to: localhost + run_once: true + changed_when: false + when: "routerbackup_diff is defined" + +- name: fill diff into file + lineinfile: + path: "{{ routerbackup_diff }}" + line: "{{ diff.stdout }}\n\n==============================================================================\n" + delegate_to: localhost + when: + - "routerbackup_diff is defined" + - "diff.stdout != ''" + +# vim: set tabstop=2 shiftwidth=2 expandtab smarttab: diff --git a/ansible-role-routerbackup/tasks/check.yml b/ansible-role-routerbackup/tasks/check.yml new file mode 100644 index 0000000..40946f3 --- /dev/null +++ b/ansible-role-routerbackup/tasks/check.yml @@ -0,0 +1,31 @@ +--- +- name: check + ansible.builtin.command: + cmd: > + {{ routerbackup_bin }}/backup_check --backupdir {{ routerbackup_dir }} + --host {{ inventory_hostname }} --maxage {{ routerbackup_maxage }} + delegate_to: localhost + changed_when: false + failed_when: false + register: check + +- name: create empty check file + copy: + content: "" + dest: "{{ routerbackup_checkfile }}" + delegate_to: localhost + run_once: true + changed_when: false + when: "routerbackup_checkfile is defined" + +- name: fill check into file + lineinfile: + path: "{{ routerbackup_checkfile }}" + line: "{{ check.stdout }}" + delegate_to: localhost + changed_when: false + when: + - "routerbackup_checkfile is defined" + - "check.stdout != ''" + +# vim: set tabstop=2 shiftwidth=2 expandtab smarttab: diff --git a/ansible-role-routerbackup/tasks/main.yml b/ansible-role-routerbackup/tasks/main.yml new file mode 100644 index 0000000..a0adb54 --- /dev/null +++ b/ansible-role-routerbackup/tasks/main.yml @@ -0,0 +1,16 @@ +--- +- name: include backup.yml + include_tasks: + file: backup.yml + apply: + tags: backup + tags: backup + +- name: include check.yml + include_tasks: + file: check.yml + apply: + tags: check + tags: check + +# vim: set tabstop=2 shiftwidth=2 expandtab smarttab: diff --git a/opt-routerbackup/backup_check b/opt-routerbackup/backup_check new file mode 100755 index 0000000..a52e68e --- /dev/null +++ b/opt-routerbackup/backup_check @@ -0,0 +1,411 @@ +#!/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(): + parser = argparse.ArgumentParser( + description='backup_check - check whether backups are current') + + 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('-m', '--maxage', + required=False, + action='store', + type=int, + help='maximum backup age in hours, default: 24') + parser.add_argument('-v', '--verbose', + required=False, + action='store_true', + help='be verbose') + + args = parser.parse_args() + return args + +def main(): + args = get_args() + if args.maxage: + maxage = args.maxage + else: + maxage = 24 + configpath = f'{args.backupdir}/{args.host}.config' + + try: + info = os.stat(configpath) + backuptime = info.st_mtime + except FileNotFoundError: + print(f'Last backup: never (backup file not found: {configpath})') + exit(1) + + age = (time.time() - backuptime) / 3600 + if age > 24: + agetext = f'{age // 24:.0f}d{age % 24:.0f}h' + else: + agetext = f'{age:.1f}h' + backuptimestamp = datetime.datetime.fromtimestamp(backuptime).strftime('%F %H:%M:%S') + if age > args.maxage: + print(f'WARNING: {args.host}: last backup {backuptimestamp}, {agetext} ago (>{maxage}h)') + exit(1) + 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/opt-routerbackup/routerbackup b/opt-routerbackup/routerbackup new file mode 100755 index 0000000..b1a0282 --- /dev/null +++ b/opt-routerbackup/routerbackup @@ -0,0 +1,413 @@ +#!/usr/bin/env python3 +import argparse +import os +import re +import logging +import paramiko +import datetime +import glob +import shutil +import time +import difflib + + +def get_args(): + parser = argparse.ArgumentParser( + description='routerbackup - save routeros/ios/asa router configuration') + + parser.add_argument('-t', '--type', + required=True, + action='store', + help='device type {routeros,ios,asa}') + parser.add_argument('-H', '--host', + required=True, + action='store', + help='host name, used also as backup filename base ') + parser.add_argument('-u', '--user', + required=True, + action='store', + help='user name') + parser.add_argument('-p', '--password', + required=False, + action='store', + help='password') + parser.add_argument('-i', '--sshkey', + required=False, + action='store', + help='ssh private key file') + parser.add_argument('-o', '--backupdir', + required=True, + action='store', + help='backup output directory') + parser.add_argument('-a', '--address', + required=False, + action='store', + help='host address (default: hostname)') + parser.add_argument('-l', '--logfile', + required=False, + action='store', + help='log file (no log if not given)') + parser.add_argument('-L', '--loglevel', + required=False, + action='store', + help='log level') + parser.add_argument('-d', '--diffprint', + required=False, + action='store_true', + help='print diff if configuration has changed') + parser.add_argument('-D', '--difflog', + required=False, + action='store_true', + help='write diff to log when configuration has changed') + parser.add_argument('-E', '--noerrors', + required=False, + action='store_true', + help='don\'t print any error messages') + + args = parser.parse_args() + return args + + +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.backupdir) + elif args.type == 'ios': + cf = IosConfig(workdir=args.backupdir) + elif args.type == 'asa': + cf = AsaConfig(workdir=args.backupdir) + + 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/routerbackup.yml b/routerbackup.yml new file mode 100644 index 0000000..18382ce --- /dev/null +++ b/routerbackup.yml @@ -0,0 +1,19 @@ +--- +- name: set router configuration backup directory + hosts: routeros,ciscoasa,!ciscoasa_slave,ciscoios + gather_facts: no + tags: always + tasks: + - set_fact: + routerbackup_bin: /opt/routerbackup + routerbackup_dir: /data/routerbackup + routerbackup_log: /var/log/routerbackup.log + routerbackup_maxage: 24 + +- name: use router backup role + hosts: routeros,ciscoasa,!ciscoasa_slave,ciscoios + gather_facts: no + roles: + - routerbackup + +# vim: set tabstop=2 shiftwidth=2 expandtab smarttab: