backup: Script to do backups
--HG-- extra : amend_source : 4ca567426083c2760d4a11551bb5398e7b25ac6emaster
parent
79355a87ea
commit
75aced9aca
|
@ -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', '<stdout>'):
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
elif log_file in ('/dev/stderr', '<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()
|
Loading…
Reference in New Issue