diff options
Diffstat (limited to 'restic.py')
| -rw-r--r-- | restic.py | 131 |
1 files changed, 131 insertions, 0 deletions
diff --git a/restic.py b/restic.py new file mode 100644 index 0000000..8033103 --- /dev/null +++ b/restic.py @@ -0,0 +1,131 @@ +""" +BSD 2-Clause License +restic subprocess wrapper +Copyright (c) 2018, Max Resnick <max@ofmax.li> +All rights reserved. + +restic-py is simple wrapper to restic binary for backups and monitoring +""" +import os +import tempfile +import configparser +import glob +import argparse +import logging +import sys +import time +import io + +import sh + +# logging +LOGLEVELS = {'DEBUG': logging.DEBUG, + 'INFO': logging.INFO, + 'WARNING': logging.WARNING, + 'ERROR': logging.ERROR} +# TODO should be a configurable location +logging.basicConfig(filename='restic.log', + level=LOGLEVELS[os.getenv('RESTIC_LOGLEVEL', 'WARNING')]) +log = logging.getLogger(__name__) +# cmd line parser +parser = argparse.ArgumentParser(description='wrapper to restic') +parser.add_argument('cmd', + type=str, + choices=('backup', 'prune', 'check', 'watch'), + help='run backup') + +# config +cfg = configparser.ConfigParser() +cfg.read('restic.ini') + +# baked commands +HOME_DIR = os.path.expanduser('~') +PASSWORD_FILE = '{}/.ssh/resticpass'.format(HOME_DIR) +BACKUP_CMD = sh.restic.bake('backup', + '--password-file', PASSWORD_FILE, + '--exclude-file', cfg['restic']['exclude'], + '--exclude-caches', + '--repo', cfg['restic']['repo_uri']) +PRUNE_CMD = sh.restic.bake('forget', + '--password-file', PASSWORD_FILE, + '--repo', cfg['restic']['repo_uri'], + '--keep-daily', 7, + '--keep-weekly', 5, + '--keep-monthly', 12, + '--keep-yearly', 75) +CHECK_CMD = sh.restic.bake('check', + '--password-file', PASSWORD_FILE, + '--repo', cfg['restic']['repo_uri']) + + +def _check_auth(): + try: + os.environ['SSH_AUTH_SOCK'] + except KeyError: + log.error('ssh agent not running') + + +def run_backup(): + directories = [] + for chunk in glob.iglob('{}/*'.format(HOME_DIR)): + if os.path.isdir(chunk): + try: + out = BACKUP_CMD(chunk) + directories.append(chunk) + log.info('{} {}'.format('restic-run', chunk)) + except sh.ErrorReturnCode_1 as e: + log.error('{} {} {}'.format('restic-run', chunk, e)) + # backs up non chunked files + with tempfile.NamedTemporaryFile(delete=True) as tmp: + tmp.write('\n'.join(directories).encode()) + out = BACKUP_CMD('--exclude-file', tmp.name, HOME_DIR) + log.info('{} {}'.format('restic-run', HOME_DIR)) + + +def run_prune(): + try: + out = PRUNE_CMD() + except sh.ErrorReturnCode_1 as e: + log.error('{} {}'.format('restic-prune', e)) + + +def run_check(): + try: + out = CHECK_CMD() + except sh.ErrorReturnCode_1 as e: + log.error('{} {}'.format('restic-check', e)) + + +def follow(logfile, pattern): + # catch up + for line in logfile.readlines(): + if pattern in line: + yield line + logfile.seek(0, 2) + while True: + line = logfile.readline() + if not line: + time.sleep(0.1) + continue + if pattern in line: + yield line + + +def run_watch(): + logfile = open("restic.log","r") + loglines = follow(logfile, 'ERROR') + # TODO notification + for line in loglines: + print(line) + + +def main(): + _check_auth() + cmd_arg = parser.parse_args() + cmd = 'run_{}'.format(cmd_arg.cmd) + try: + run = globals()[cmd] + except KeyError: + log.error('command {} not found'.format(cmd)) + sys.exit(1) + run() |