scripts/backup.py

184 lines
6.6 KiB
Python
Executable File

#!/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 = ['-rtO', '--partial', '--delete']
RSYNC_EXTRA_ARGS = shlex.split(os.environ.get('RSYNC_EXTRA_ARGS', ''))
DEFAULT_CONFIG_FILENAME = 'backups.ini'
log = logging.getLogger('backup')
def __init__(self, destination, config=None, pretend=False):
self.config = configparser.ConfigParser()
if config:
self.config.read_file(config)
else:
with open(self.default_config) as config:
self.config.read_file(config)
self.config.filename = config.name
self.destination = destination
self.pretend = pretend
self.stdout = sys.stdout
self.stderr = sys.stderr
@property
def default_config(self):
try:
config_dir = os.environ['XDG_CONFIG_DIR']
except KeyError:
config_dir = os.path.expanduser('~/.config')
return os.path.join(config_dir, self.DEFAULT_CONFIG_FILENAME)
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)))
devnull = open(os.devnull)
try:
subprocess.check_call(cmd, stdin=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')
finally:
devnull.close()
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('--quiet', '-q', action='store_true', default=False,
help='Do not print informational messages')
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', '-c', type=argparse.FileType('r'),
metavar='FILENAME',
help='Path to configuration file')
parser.add_argument('destination',
help='Backup destination directory')
return parser.parse_args()
def main():
args = _parse_args()
backup = Backup(args.destination, args.config, args.pretend)
if args.config:
args.config.close()
if not args.quiet:
print('Backing up to {} using configuration from {}'.format(
args.destination, backup.config.filename))
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\n'.format(args.log_file))
raise SystemExit(1)
if not args.quiet:
print('Backup completed successfully')
if __name__ == '__main__':
main()