From 75aced9acab8fdf3fb9dddfa5fbdd705c1dec5ed Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Sun, 12 Oct 2014 17:50:04 -0500 Subject: [PATCH] backup: Script to do backups --HG-- extra : amend_source : 4ca567426083c2760d4a11551bb5398e7b25ac6e --- backup.py | 157 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100755 backup.py diff --git a/backup.py b/backup.py new file mode 100755 index 0000000..846e107 --- /dev/null +++ b/backup.py @@ -0,0 +1,157 @@ +#!/usr/bin/python +import argparse +import configparser +import logging +import os +import shlex +import subprocess +import sys + + +class BackupError(Exception): + '''Raised when an error occurs backing up an item''' + + +class Backup(object): + + RSYNC = os.environ.get('RSYNC', 'rsync') + RSYNC_DEFAULT_ARGS = ['-aO', '--partial'] + RSYNC_EXTRA_ARGS = shlex.split(os.environ.get('RSYNC_EXTRA_ARGS', '')) + + log = logging.getLogger('backup') + + def __init__(self, config, destination, pretend=False): + self.config = config + self.destination = destination + self.pretend = pretend + self.stdout = sys.stdout + self.stderr = sys.stderr + + def logsetup(self, log_file=None, log_level='INFO'): + if not log_file: + return + logger = logging.getLogger() + logger.setLevel(logging.NOTSET) + if log_file in ('-', '/dev/stdout', ''): + handler = logging.StreamHandler(sys.stdout) + elif log_file in ('/dev/stderr', ''): + handler = logging.StreamHandler(sys.stderr) + else: + handler = logging.FileHandler(log_file) + handler.setFormatter(logging.Formatter( + '%(asctime)s [%(name)s] %(levelname)s %(message)s')) + handler.setLevel(getattr(logging, log_level, logging.INFO)) + logger.addHandler(handler) + self.stdout = self.stderr = handler.stream + + def backup_all(self, include=None, exclude=None): + success = True + self.log.info('Starting backup') + if self.pretend: + self.log.warning('Pretend mode: no files will be copied!') + for section in self.config: + if section != self.config.default_section: + if exclude and section in exclude: + self.log.debug('Excluded section {}'.format(section)) + continue + if include and section not in include: + self.log.debug('Section {} not included'.format(section)) + continue + try: + self.backup(section) + except BackupError as e: + self.log.error('{}'.format(e)) + success = False + except: + self.log.exception( + 'Unexpected error backing up {}'.format(section)) + success = False + if success: + self.log.info('Backup completed successfully') + else: + self.log.warning('Backup completed with at least one error') + return success + + def backup(self, section): + log = logging.getLogger('backup.{}'.format(section)) + try: + item = self.config[section] + except KeyError: + raise BackupError('Unknown backup item {}'.format(section)) + + try: + source = item['source'] + except KeyError: + raise BackupError('Missing source for {}'.format(s.name)) + try: + destination = item['destination'] + except KeyError: + src_path = source.split(':', 1)[-1] + if src_path.endswith('/'): + src_path = src_path.rstrip('/') + destination = os.path.basename(src_path) + destination = os.path.join(self.destination, destination) + + args = [self.RSYNC] + args.extend(self.RSYNC_DEFAULT_ARGS) + args.extend(self.RSYNC_EXTRA_ARGS) + try: + exclude = item['exclude'] + except KeyError: + pass + else: + for e in shlex.split(exclude): + args.extend(('--exclude', e)) + args.extend((source, destination)) + if self.pretend: + args.append('-n') + log.info('Backing up {} to {}'.format(source, destination)) + self._run(*args) + log.info('Backup complete') + + def _run(self, *cmd): + self.log.debug('Running command: {}'.format(' '.join(cmd))) + try: + subprocess.check_call(cmd, stdin=open(os.devnull), + stdout=self.stdout, stderr=self.stderr) + except (OSError, subprocess.CalledProcessError) as e: + raise BackupError('Error executing rsync: {}'.format(e)) + except KeyboardInterrupt: + raise BackupError('rsync interrupted') + + +def _parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument('--log-file', '-l', default='-', metavar='FILENAME', + help='Log file path') + parser.add_argument('--log-level', '-D', default='INFO', metavar='LEVEL', + help='Log level') + parser.add_argument('--pretend', '-p', action='store_true', default=False, + help='Execute a dry run') + parser.add_argument('--include', '-I', action='append', + help='Only back up specific items') + parser.add_argument('--exclude', '-X', action='append', + help='Do not back up specific items') + parser.add_argument('config', type=argparse.FileType('r'), + metavar='FILENAME', + help='Path to configuration file') + parser.add_argument('destination', metavar='FILENAME', + help='Backup destination directory') + return parser.parse_args() + + +def main(): + args = _parse_args() + config = configparser.ConfigParser() + config.read_file(args.config) + backup = Backup(config, args.destination, args.pretend) + backup.logsetup(args.log_file, args.log_level) + if not backup.backup_all(args.include, args.exclude): + sys.stderr.write('Errors occurred during backup\n') + if args.log_file and args.log_file != '-': + sys.stderr.write('See {} for details'.format(args.log_file)) + raise SystemExit(1) + + +if __name__ == '__main__': + main()