aboutsummaryrefslogtreecommitdiff
path: root/restic.py
diff options
context:
space:
mode:
Diffstat (limited to 'restic.py')
-rw-r--r--restic.py131
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()