adding restore functionality
This commit is contained in:
		
							parent
							
								
									b566970d57
								
							
						
					
					
						commit
						120b576a38
					
				@ -3,6 +3,8 @@
 | 
			
		||||
# TODO: https://borgbackup.readthedocs.io/en/latest/internals/frontends.html
 | 
			
		||||
# will they EVER release a public API? for now we'll just use subprocess since
 | 
			
		||||
# we import it for various prep stuff anyways.
 | 
			
		||||
# TODO: change loglevel of borg itself in subprocess to match the argparse?
 | 
			
		||||
# --debug, --info (same as -v/--verbose), --warning, --error, --critical
 | 
			
		||||
 | 
			
		||||
import argparse
 | 
			
		||||
import configparser
 | 
			
		||||
@ -13,6 +15,7 @@ import logging.handlers
 | 
			
		||||
import os
 | 
			
		||||
import subprocess
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
try:
 | 
			
		||||
    import pymysql  # not stdlib; "python-pymysql" in Arch's AUR
 | 
			
		||||
    has_mysql = True
 | 
			
		||||
@ -26,11 +29,13 @@ except ImportError:
 | 
			
		||||
    has_systemd = False
 | 
			
		||||
 | 
			
		||||
### LOG LEVEL MAPPINGS ###
 | 
			
		||||
loglvls = {'critical': logging.CRITICAL,
 | 
			
		||||
           'error': logging.ERROR,
 | 
			
		||||
           'warning': logging.WARNING,
 | 
			
		||||
           'info': logging.INFO,
 | 
			
		||||
           'debug': logging.DEBUG}
 | 
			
		||||
loglvls = {
 | 
			
		||||
    'critical': logging.CRITICAL,
 | 
			
		||||
    'error': logging.ERROR,
 | 
			
		||||
    'warning': logging.WARNING,
 | 
			
		||||
    'info': logging.INFO,
 | 
			
		||||
    'debug': logging.DEBUG}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### THE GUTS ###
 | 
			
		||||
class Backup(object):
 | 
			
		||||
@ -48,21 +53,26 @@ class Backup(object):
 | 
			
		||||
        # and user K900_ on r/python for entertaining my very silly question.
 | 
			
		||||
        self.logger = logging.getLogger(__name__)
 | 
			
		||||
        self.logger.setLevel(loglvls[self.args['loglevel']])
 | 
			
		||||
        _logfmt = logging.Formatter(fmt = '{levelname}:{name}: {message} ({asctime}; {filename}:{lineno})',
 | 
			
		||||
                                    style = '{',
 | 
			
		||||
                                    datefmt = '%Y-%m-%d %H:%M:%S')
 | 
			
		||||
        _journalfmt = logging.Formatter(fmt = '{levelname}:{name}: {message} ({filename}:{lineno})',
 | 
			
		||||
                                    style = '{',
 | 
			
		||||
                                    datefmt = '%Y-%m-%d %H:%M:%S')
 | 
			
		||||
        _logfmt = logging.Formatter(
 | 
			
		||||
                fmt = ('{levelname}:{name}: {message} ({asctime}; '
 | 
			
		||||
                       '{filename}:{lineno})'),
 | 
			
		||||
                style = '{',
 | 
			
		||||
                datefmt = '%Y-%m-%d %H:%M:%S')
 | 
			
		||||
        _journalfmt = logging.Formatter(
 | 
			
		||||
                fmt = '{levelname}:{name}: {message} ({filename}:{lineno})',
 | 
			
		||||
                style = '{',
 | 
			
		||||
                datefmt = '%Y-%m-%d %H:%M:%S')
 | 
			
		||||
        handlers = []
 | 
			
		||||
        if self.args['disklog']:
 | 
			
		||||
            os.makedirs(os.path.dirname(self.args['logfile']),
 | 
			
		||||
                        exist_ok = True,
 | 
			
		||||
                        mode = 0o700)
 | 
			
		||||
            handlers.append(logging.handlers.RotatingFileHandler(self.args['logfile'],
 | 
			
		||||
                                                                 encoding = 'utf8',
 | 
			
		||||
                                                                 maxBytes = 100000,
 | 
			
		||||
                                                                 backupCount = 1))
 | 
			
		||||
            # TODO: make the constraints for rotation in config?
 | 
			
		||||
            handlers.append(
 | 
			
		||||
                    logging.handlers.RotatingFileHandler(self.args['logfile'],
 | 
			
		||||
                                                         encoding = 'utf8',
 | 
			
		||||
                                                         maxBytes = 100000,
 | 
			
		||||
                                                         backupCount = 1))
 | 
			
		||||
        if self.args['verbose']:
 | 
			
		||||
            handlers.append(logging.StreamHandler())
 | 
			
		||||
        if has_systemd:
 | 
			
		||||
@ -77,7 +87,8 @@ class Backup(object):
 | 
			
		||||
        self.logger.debug('BEGIN INITIALIZATION')
 | 
			
		||||
        ### CONFIG ###
 | 
			
		||||
        if not os.path.isfile(self.args['cfgfile']):
 | 
			
		||||
            self.logger.error('{0} does not exist'.format(self.args['cfgfile']))
 | 
			
		||||
            self.logger.error(
 | 
			
		||||
                    '{0} does not exist'.format(self.args['cfgfile']))
 | 
			
		||||
            exit(1)
 | 
			
		||||
        with open(self.args['cfgfile'], 'r') as f:
 | 
			
		||||
            self.cfg = json.loads(f.read())
 | 
			
		||||
@ -91,7 +102,9 @@ class Backup(object):
 | 
			
		||||
            if r == 'all':
 | 
			
		||||
                self.args['repo'].remove(r)
 | 
			
		||||
            elif r not in self.cfg['repos'].keys():
 | 
			
		||||
                self.logger.warning('Repository {0} is not configured; skipping.'.format(r))
 | 
			
		||||
                self.logger.warning(
 | 
			
		||||
                        'Repository {0} is not configured; skipping.'.format(
 | 
			
		||||
                                r))
 | 
			
		||||
                self.args['repo'].remove(r)
 | 
			
		||||
        self.logger.debug('VARS (after args cleanup): {0}'.format(vars()))
 | 
			
		||||
        self.logger.debug('END INITIALIZATION')
 | 
			
		||||
@ -102,13 +115,14 @@ class Backup(object):
 | 
			
		||||
        else:
 | 
			
		||||
            self.cron = True
 | 
			
		||||
        ### END INIT ###
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    def cmdExec(self, cmd, stdoutfh = None):
 | 
			
		||||
        self.logger.debug('Running command: {0}'.format(' '.join(cmd)))
 | 
			
		||||
        if self.args['dryrun']:
 | 
			
		||||
            return()  # no-op
 | 
			
		||||
            return ()  # no-op
 | 
			
		||||
        if stdoutfh:
 | 
			
		||||
            _cmd = subprocess.run(cmd, stdout = stdoutfh, stderr = subprocess.PIPE)
 | 
			
		||||
            _cmd = subprocess.run(cmd, stdout = stdoutfh,
 | 
			
		||||
                                  stderr = subprocess.PIPE)
 | 
			
		||||
        else:
 | 
			
		||||
            _cmd = subprocess.run(cmd,
 | 
			
		||||
                                  stdout = subprocess.PIPE,
 | 
			
		||||
@ -119,9 +133,10 @@ class Backup(object):
 | 
			
		||||
        if _returncode != 0:
 | 
			
		||||
            self.logger.error('STDERR: ({1})\n{0}'.format(_err, ' '.join(cmd)))
 | 
			
		||||
        if _err != '' and self.cron:
 | 
			
		||||
            self.logger.warning('Command {0} failed: {1}'.format(' '.join(cmd), _err))
 | 
			
		||||
            self.logger.warning(
 | 
			
		||||
                    'Command {0} failed: {1}'.format(' '.join(cmd), _err))
 | 
			
		||||
        return()
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    def createRepo(self):
 | 
			
		||||
        _env = os.environ.copy()
 | 
			
		||||
        _env['BORG_RSH'] = self.cfg['config']['ctx']
 | 
			
		||||
@ -149,16 +164,21 @@ class Backup(object):
 | 
			
		||||
                # sigh. borg uses stderr for verbose output.
 | 
			
		||||
                self.logger.debug('[{0}]: STDERR: ({2})\n{1}'.format(r,
 | 
			
		||||
                                                                     _stderr,
 | 
			
		||||
                                                                     ' '.join(_cmd)))
 | 
			
		||||
                                                                     ' '.join(
 | 
			
		||||
                                                                             _cmd)))
 | 
			
		||||
                if _returncode != 0:
 | 
			
		||||
                    self.logger.error('[{0}]: FAILED: {1}'.format(r, ' '.join(_cmd)))
 | 
			
		||||
                    self.logger.error(
 | 
			
		||||
                            '[{0}]: FAILED: {1}'.format(r, ' '.join(_cmd)))
 | 
			
		||||
                if _err != '' and self.cron and _returncode != 0:
 | 
			
		||||
                    self.logger.warning('Command {0} failed: {1}'.format(' '.join(cmd), _err))
 | 
			
		||||
            del(_env['BORG_PASSPHRASE'])
 | 
			
		||||
                    self.logger.warning(
 | 
			
		||||
                            'Command {0} failed: {1}'.format(' '.join(cmd),
 | 
			
		||||
                                                             _err))
 | 
			
		||||
            del (_env['BORG_PASSPHRASE'])
 | 
			
		||||
            self.logger.info('[{0}]: END INITIALIZATION'.format(r))
 | 
			
		||||
        return()
 | 
			
		||||
 | 
			
		||||
    def create(self):
 | 
			
		||||
        # TODO: support "--strip-components N"?
 | 
			
		||||
        _env = os.environ.copy()
 | 
			
		||||
        _env['BORG_RSH'] = self.cfg['config']['ctx']
 | 
			
		||||
        self.logger.info('START: backup')
 | 
			
		||||
@ -166,9 +186,12 @@ class Backup(object):
 | 
			
		||||
            self.logger.info('[{0}]: BEGIN BACKUP'.format(r))
 | 
			
		||||
            if 'prep' in self.cfg['repos'][r].keys():
 | 
			
		||||
                for prep in self.cfg['repos'][r]['prep']:
 | 
			
		||||
                    self.logger.info('[{0}]: Running prepfunc {1}'.format(r, prep))
 | 
			
		||||
                    eval('self.{0}'.format(prep))  # I KNOW, IT'S TERRIBLE. so sue me.
 | 
			
		||||
                    self.logger.info('[{0}]: Finished prepfunc {1}'.format(r, prep))
 | 
			
		||||
                    self.logger.info(
 | 
			
		||||
                            '[{0}]: Running prepfunc {1}'.format(r, prep))
 | 
			
		||||
                    eval('self.{0}'.format(
 | 
			
		||||
                            prep))  # I KNOW, IT'S TERRIBLE. so sue me.
 | 
			
		||||
                    self.logger.info(
 | 
			
		||||
                            '[{0}]: Finished prepfunc {1}'.format(r, prep))
 | 
			
		||||
            _cmd = ['borg',
 | 
			
		||||
                    'create',
 | 
			
		||||
                    '-v', '--stats',
 | 
			
		||||
@ -184,9 +207,10 @@ class Backup(object):
 | 
			
		||||
                _cmd.append(p)
 | 
			
		||||
            _env['BORG_PASSPHRASE'] = self.cfg['repos'][r]['password']
 | 
			
		||||
            self.logger.debug('VARS: {0}'.format(vars()))
 | 
			
		||||
            # We don't use self.cmdExec() here because we want to explicitly pass the env
 | 
			
		||||
            # and format the log line differently.
 | 
			
		||||
            self.logger.debug('[{0}]: Running command: {1}'.format(r, ' '.join(_cmd)))
 | 
			
		||||
            # We don't use self.cmdExec() here because we want to explicitly
 | 
			
		||||
            # pass the env and format the log line differently.
 | 
			
		||||
            self.logger.debug(
 | 
			
		||||
                    '[{0}]: Running command: {1}'.format(r, ' '.join(_cmd)))
 | 
			
		||||
            if not self.args['dryrun']:
 | 
			
		||||
                _out = subprocess.run(_cmd,
 | 
			
		||||
                                      env = _env,
 | 
			
		||||
@ -198,16 +222,73 @@ class Backup(object):
 | 
			
		||||
                self.logger.debug('[{0}]: (RESULT) {1}'.format(r, _stdout))
 | 
			
		||||
                self.logger.error('[{0}]: STDERR: ({2})\n{1}'.format(r,
 | 
			
		||||
                                                                     _stderr,
 | 
			
		||||
                                                                     ' '.join(_cmd)))
 | 
			
		||||
                                                                     ' '.join(
 | 
			
		||||
                                                                             _cmd)))
 | 
			
		||||
                if _returncode != 0:
 | 
			
		||||
                    self.logger.error('[{0}]: FAILED: {1}'.format(r, ' '.join(_cmd)))
 | 
			
		||||
                    self.logger.error(
 | 
			
		||||
                            '[{0}]: FAILED: {1}'.format(r, ' '.join(_cmd)))
 | 
			
		||||
                if _stderr != '' and self.cron and _returncode != 0:
 | 
			
		||||
                    self.logger.warning('Command {0} failed: {1}'.format(' '.join(_cmd), _stderr))
 | 
			
		||||
                del(_env['BORG_PASSPHRASE'])
 | 
			
		||||
                    self.logger.warning(
 | 
			
		||||
                            'Command {0} failed: {1}'.format(' '.join(_cmd),
 | 
			
		||||
                                                             _stderr))
 | 
			
		||||
                del (_env['BORG_PASSPHRASE'])
 | 
			
		||||
            self.logger.info('[{0}]: END BACKUP'.format(r))
 | 
			
		||||
        self.logger.info('END: backup')
 | 
			
		||||
        return()
 | 
			
		||||
 | 
			
		||||
    def restore(self):
 | 
			
		||||
        # TODO: support "--strip-components N"?
 | 
			
		||||
        # TODO: support add'l args?
 | 
			
		||||
        # https://borgbackup.readthedocs.io/en/stable/usage/extract.html
 | 
			
		||||
        _env = os.environ.copy()
 | 
			
		||||
        _env['BORG_RSH'] = self.cfg['config']['ctx']
 | 
			
		||||
        self.logger.info('START: restore')
 | 
			
		||||
        for r in self.args['repo']:
 | 
			
		||||
            self.logger.info('[{0}]: BEGIN RESTORE'.format(r))
 | 
			
		||||
            _cmd = ['borg',
 | 
			
		||||
                    'extract',
 | 
			
		||||
                    '-v', '--stats',
 | 
			
		||||
                    '--compression', 'lzma,9']
 | 
			
		||||
            # if 'excludes' in self.cfg['repos'][r].keys():
 | 
			
		||||
            #     for e in self.cfg['repos'][r]['excludes']:
 | 
			
		||||
            #         _cmd.extend(['--exclude', e])
 | 
			
		||||
            _cmd.append('{0}@{1}:{2}::{3}'.format(self.cfg['config']['user'],
 | 
			
		||||
                                                  self.cfg['config']['host'],
 | 
			
		||||
                                                  r,
 | 
			
		||||
                                                  self.args['archive']))
 | 
			
		||||
            # TODO: support specific path of extract?
 | 
			
		||||
            # if so, append path(s) here.
 | 
			
		||||
            _env['BORG_PASSPHRASE'] = self.cfg['repos'][r]['password']
 | 
			
		||||
            self.logger.debug('VARS: {0}'.format(vars()))
 | 
			
		||||
            # We don't use self.cmdExec() here because we want to explicitly
 | 
			
		||||
            # pass the env and format the log line differently.
 | 
			
		||||
            self.logger.debug(
 | 
			
		||||
                    '[{0}]: Running command: {1}'.format(r, ' '.join(_cmd)))
 | 
			
		||||
            if not self.args['dryrun']:
 | 
			
		||||
                _out = subprocess.run(_cmd,
 | 
			
		||||
                                      env = _env,
 | 
			
		||||
                                      stdout = subprocess.PIPE,
 | 
			
		||||
                                      stderr = subprocess.PIPE)
 | 
			
		||||
                _stdout = _out.stdout.decode('utf-8').strip()
 | 
			
		||||
                _stderr = _out.stderr.decode('utf-8').strip()
 | 
			
		||||
                _returncode = _out.returncode
 | 
			
		||||
                self.logger.debug('[{0}]: (RESULT) {1}'.format(r, _stdout))
 | 
			
		||||
                self.logger.error('[{0}]: STDERR: ({2})\n{1}'.format(r,
 | 
			
		||||
                                                                     _stderr,
 | 
			
		||||
                                                                     ' '.join(
 | 
			
		||||
                                                                             _cmd)))
 | 
			
		||||
                if _returncode != 0:
 | 
			
		||||
                    self.logger.error(
 | 
			
		||||
                            '[{0}]: FAILED: {1}'.format(r, ' '.join(_cmd)))
 | 
			
		||||
                if _stderr != '' and self.cron and _returncode != 0:
 | 
			
		||||
                    self.logger.warning(
 | 
			
		||||
                            'Command {0} failed: {1}'.format(' '.join(_cmd),
 | 
			
		||||
                                                             _stderr))
 | 
			
		||||
                del (_env['BORG_PASSPHRASE'])
 | 
			
		||||
            self.logger.info('[{0}]: END RESTORE'.format(r))
 | 
			
		||||
        self.logger.info('END: restore')
 | 
			
		||||
        return()
 | 
			
		||||
 | 
			
		||||
    def miscBak(self, pkgr):
 | 
			
		||||
        self.logger.info('BEGIN: miscBak()')
 | 
			
		||||
        _cmd = None
 | 
			
		||||
@ -231,8 +312,9 @@ class Backup(object):
 | 
			
		||||
    def mysqlBak(self):
 | 
			
		||||
        self.logger.info('BEGIN: mysqlBak()')
 | 
			
		||||
        if not has_mysql:
 | 
			
		||||
            self.logger.error('You need to install the PyMySQL module to back up MySQL databases. Skipping.')
 | 
			
		||||
            return()
 | 
			
		||||
            self.logger.error(
 | 
			
		||||
                    'You need to install the PyMySQL module to back up MySQL databases. Skipping.')
 | 
			
		||||
            return ()
 | 
			
		||||
        # These are mysqldump options shared by ALL databases
 | 
			
		||||
        _mysqlopts = ['--routines',
 | 
			
		||||
                      '--add-drop-database',
 | 
			
		||||
@ -244,11 +326,12 @@ class Backup(object):
 | 
			
		||||
        _DBs = []
 | 
			
		||||
        _mycnf = os.path.expanduser(os.path.join('~', '.my.cnf'))
 | 
			
		||||
        if not os.path.isfile(_mycnf):
 | 
			
		||||
            exit('{0}: ERROR: Cannot get credentials for MySQL (cannot find ~/.my.cnf)!')
 | 
			
		||||
            exit(
 | 
			
		||||
                    '{0}: ERROR: Cannot get credentials for MySQL (cannot find ~/.my.cnf)!')
 | 
			
		||||
        _mycfg = configparser.ConfigParser()
 | 
			
		||||
        _mycfg._interpolation = configparser.ExtendedInterpolation()
 | 
			
		||||
        _mycfg.read(_mycnf)
 | 
			
		||||
        _sqlcfg = {s:dict(_mycfg.items(s)) for s in _mycfg.sections()}
 | 
			
		||||
        _sqlcfg = {s: dict(_mycfg.items(s)) for s in _mycfg.sections()}
 | 
			
		||||
        if 'host' not in _sqlcfg.keys():
 | 
			
		||||
            _socketpath = '/var/run/mysqld/mysqld.sock'  # correct for Arch, YMMV.
 | 
			
		||||
            _mysql = pymysql.connect(unix_socket = _socketpath,
 | 
			
		||||
@ -266,7 +349,8 @@ class Backup(object):
 | 
			
		||||
        self.logger.debug('Databases: {0}'.format(', '.join(_DBs)))
 | 
			
		||||
        for db in _DBs:
 | 
			
		||||
            _cmd = ['mysqldump',
 | 
			
		||||
                    '--result-file={0}.sql'.format(os.path.join(self.args['mysqldir'], db))]
 | 
			
		||||
                    '--result-file={0}.sql'.format(
 | 
			
		||||
                            os.path.join(self.args['mysqldir'], db))]
 | 
			
		||||
            # These are database-specific options
 | 
			
		||||
            if db in ('information_schema', 'performance_schema'):
 | 
			
		||||
                _cmd.append('--skip-lock-tables')
 | 
			
		||||
@ -284,7 +368,9 @@ class Backup(object):
 | 
			
		||||
        if self.args['verbose']:
 | 
			
		||||
            print('\033[1mDETAILS:\033[0m\n')
 | 
			
		||||
            for r in self.args['repo']:
 | 
			
		||||
                print('\t\033[1m{0}:\033[0m\n\t\t\033[1mPath(s):\033[0m\t'.format(r.upper()), end = '')
 | 
			
		||||
                print(
 | 
			
		||||
                        '\t\033[1m{0}:\033[0m\n\t\t\033[1mPath(s):\033[0m\t'.format(
 | 
			
		||||
                                r.upper()), end = '')
 | 
			
		||||
                for p in self.cfg['repos'][r]['paths']:
 | 
			
		||||
                    print(p, end = ' ')
 | 
			
		||||
                if 'prep' in self.cfg['repos'][r].keys():
 | 
			
		||||
@ -307,15 +393,19 @@ class Backup(object):
 | 
			
		||||
                print(r, end = '')
 | 
			
		||||
                for line in _results[r]:
 | 
			
		||||
                    _snapshot = line.split()
 | 
			
		||||
                    print('\t{0}\t\t{1}'.format(_snapshot[0], ' '.join(_snapshot[1:])))
 | 
			
		||||
                    print('\t{0}\t\t{1}'.format(_snapshot[0],
 | 
			
		||||
                                                ' '.join(_snapshot[1:])))
 | 
			
		||||
                print()
 | 
			
		||||
        else:  # It's a listing inside an archive
 | 
			
		||||
            if self.args['verbose']:
 | 
			
		||||
                _fields = ['REPO:', 'PERMS:', 'OWNERSHIP:', 'SIZE:', 'TIMESTAMP:', 'PATH:']
 | 
			
		||||
                _fields = ['REPO:', 'PERMS:', 'OWNERSHIP:', 'SIZE:',
 | 
			
		||||
                           'TIMESTAMP:', 'PATH:']
 | 
			
		||||
                for r in _results.keys():
 | 
			
		||||
                    print('\033[1m{0}\t{1}\033[0m'.format(_fields[0], r))
 | 
			
		||||
                    # https://docs.python.org/3/library/string.html#formatspec
 | 
			
		||||
                    print('{0[1]:<15}\t{0[2]:<15}\t{0[3]:<15}\t{0[4]:<24}\t{0[5]:<15}'.format(_fields))
 | 
			
		||||
                    print(
 | 
			
		||||
                            '{0[1]:<15}\t{0[2]:<15}\t{0[3]:<15}\t{0[4]:<24}\t{0[5]:<15}'.format(
 | 
			
		||||
                                    _fields))
 | 
			
		||||
                    for line in _results[r]:
 | 
			
		||||
                        _fline = line.split()
 | 
			
		||||
                        _perms = _fline[0]
 | 
			
		||||
@ -323,11 +413,13 @@ class Backup(object):
 | 
			
		||||
                        _size = _fline[3]
 | 
			
		||||
                        _time = ' '.join(_fline[4:7])
 | 
			
		||||
                        _path = ' '.join(_fline[7:])
 | 
			
		||||
                        print('{0:<15}\t{1:<15}\t{2:<15}\t{3:<24}\t{4:<15}'.format(_perms,
 | 
			
		||||
                                                                                   _ownership,
 | 
			
		||||
                                                                                   _size,
 | 
			
		||||
                                                                                   _time,
 | 
			
		||||
                                                                                   _path))
 | 
			
		||||
                        print(
 | 
			
		||||
                                '{0:<15}\t{1:<15}\t{2:<15}\t{3:<24}\t{4:<15}'.format(
 | 
			
		||||
                                        _perms,
 | 
			
		||||
                                        _ownership,
 | 
			
		||||
                                        _size,
 | 
			
		||||
                                        _time,
 | 
			
		||||
                                        _path))
 | 
			
		||||
            else:
 | 
			
		||||
                print('\033[1mREPO:\tPATH:\033[0m\n')
 | 
			
		||||
                for r in _results.keys():
 | 
			
		||||
@ -360,173 +452,249 @@ class Backup(object):
 | 
			
		||||
            if not self.args['dryrun']:
 | 
			
		||||
 | 
			
		||||
                _out = subprocess.run(_cmd,
 | 
			
		||||
                                       env = _env,
 | 
			
		||||
                                       stdout = subprocess.PIPE,
 | 
			
		||||
                                       stderr = subprocess.PIPE)
 | 
			
		||||
                _stdout = [i.strip() for i in _out.stdout.decode('utf-8').splitlines()]
 | 
			
		||||
                                      env = _env,
 | 
			
		||||
                                      stdout = subprocess.PIPE,
 | 
			
		||||
                                      stderr = subprocess.PIPE)
 | 
			
		||||
                _stdout = [i.strip() for i in
 | 
			
		||||
                           _out.stdout.decode('utf-8').splitlines()]
 | 
			
		||||
                _stderr = _out.stderr.decode('utf-8').strip()
 | 
			
		||||
                _returncode = _out.returncode
 | 
			
		||||
                output[r] = _stdout
 | 
			
		||||
                self.logger.debug('[{0}]: (RESULT) {1}'.format(r,
 | 
			
		||||
                                                               '\n'.join(_stdout)))
 | 
			
		||||
                                                               '\n'.join(
 | 
			
		||||
                                                                       _stdout)))
 | 
			
		||||
                if _returncode != 0:
 | 
			
		||||
                    self.logger.error('[{0}]: STDERR: ({2}) ({1})'.format(r,
 | 
			
		||||
                                                                        _stderr,
 | 
			
		||||
                                                                        ' '.join(_cmd)))
 | 
			
		||||
                                                                          _stderr,
 | 
			
		||||
                                                                          ' '.join(
 | 
			
		||||
                                                                                  _cmd)))
 | 
			
		||||
                if _stderr != '' and self.cron and _returncode != 0:
 | 
			
		||||
                    self.logger.warning('Command {0} failed: {1}'.format(' '.join(cmd), _err))
 | 
			
		||||
            del(_env['BORG_PASSPHRASE'])
 | 
			
		||||
                    self.logger.warning(
 | 
			
		||||
                            'Command {0} failed: {1}'.format(' '.join(cmd),
 | 
			
		||||
                                                             _err))
 | 
			
		||||
            del (_env['BORG_PASSPHRASE'])
 | 
			
		||||
            if not self.args['archive']:
 | 
			
		||||
                if self.args['numlimit'] > 0:
 | 
			
		||||
                    if self.args['old']:
 | 
			
		||||
                        output[r] = output[r][:self.args['numlimit']]
 | 
			
		||||
                    else:
 | 
			
		||||
                        output[r] = list(reversed(output[r]))[:self.args['numlimit']]
 | 
			
		||||
                        output[r] = list(reversed(output[r]))[
 | 
			
		||||
                                    :self.args['numlimit']]
 | 
			
		||||
            if self.args['invert']:
 | 
			
		||||
                output[r] = reversed(output[r])
 | 
			
		||||
        self.logger.debug('END: lister')
 | 
			
		||||
        return(output)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def printMoarHelp():
 | 
			
		||||
    _helpstr = ('\n\tNOTE: Sorting only applies to listing archives, NOT the contents!\n\n' +
 | 
			
		||||
                'In order to efficiently display results, there are several options to handle it. ' +
 | 
			
		||||
                'Namely, these are:\n\n\t\t-s/--sort [direction]\n\t\t-l/--limit [number]\n\t\t-x/--invert\n\n' +
 | 
			
		||||
                'For example, if you want to list the 5 most recently *taken* snapshots, you would use:\n\n\t\t-l 5\n\n' +
 | 
			
		||||
                'If you would want those SAME results SORTED in the reverse order (i.e. the 5 most recently ' +
 | 
			
		||||
                'taken snapshots sorted from newest to oldest), then it would be: \n\n\t\t-l 5 -x\n\n' +
 | 
			
		||||
                'Lastly, if you wanted to list the 7 OLDEST TAKEN snapshots in reverse order (that is, ' +
 | 
			
		||||
                'sorted from newest to oldest), that\'d be: \n\n\t\t-o -l 7 -x\n')
 | 
			
		||||
    _helpstr = (
 | 
			
		||||
        '\n\tNOTE: Sorting only applies to listing archives, NOT the '
 | 
			
		||||
        'contents!\n\n'
 | 
			
		||||
        'In order to efficiently display results, there are several options '
 | 
			
		||||
        'to handle it. Namely, these are:\n\n\t\t'
 | 
			
		||||
        '-s/--sort [direction]\n\t\t'
 | 
			
		||||
        '-l/--limit [number]\n\t\t'
 | 
			
		||||
        '-x/--invert\n\n'
 | 
			
		||||
        'For example, if you want to list the 5 most recently *taken* '
 | 
			
		||||
        'snapshots, you would use:\n\n\t\t'
 | 
			
		||||
        '-l 5\n\n'
 | 
			
		||||
        'If you would want those SAME results SORTED in the reverse order '
 | 
			
		||||
        '(i.e. the 5 most recently taken snapshots sorted from newest to '
 | 
			
		||||
        'oldest), then it would be: \n\n\t\t'
 | 
			
		||||
        '-l 5 -x\n\n'
 | 
			
		||||
        'Lastly, if you wanted to list the 7 OLDEST TAKEN snapshots in '
 | 
			
		||||
        'reverse order (that is, sorted from newest to oldest), that\'d be: '
 | 
			
		||||
        '\n\n\t\t'
 | 
			
		||||
        '-o -l 7 -x\n')
 | 
			
		||||
    print(_helpstr)
 | 
			
		||||
    exit(0)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def parseArgs():
 | 
			
		||||
    ### DEFAULTS ###
 | 
			
		||||
    _date = datetime.datetime.now().strftime("%Y_%m_%d.%H_%M")
 | 
			
		||||
    _logfile = '/var/log/borg/{0}'.format(_date)
 | 
			
		||||
    _mysqldir = os.path.abspath(os.path.join(os.path.expanduser('~'), '.bak', 'mysql'))
 | 
			
		||||
    _stagedir = os.path.abspath(os.path.join(os.path.expanduser('~'), '.bak', 'misc'))
 | 
			
		||||
    _cfgfile = os.path.abspath(os.path.join(os.path.expanduser('~'), '.config', 'optools', 'backup.json'))
 | 
			
		||||
    _mysqldir = os.path.abspath(
 | 
			
		||||
            os.path.join(os.path.expanduser('~'),
 | 
			
		||||
                         '.bak',
 | 
			
		||||
                         'mysql'))
 | 
			
		||||
    _stagedir = os.path.abspath(
 | 
			
		||||
            os.path.join(os.path.expanduser('~'),
 | 
			
		||||
                         '.bak',
 | 
			
		||||
                         'misc'))
 | 
			
		||||
    _cfgfile = os.path.abspath(
 | 
			
		||||
            os.path.join(os.path.expanduser('~'),
 | 
			
		||||
                         '.config',
 | 
			
		||||
                         'optools',
 | 
			
		||||
                         'backup.json'))
 | 
			
		||||
    _defloglvl = 'info'
 | 
			
		||||
    ######
 | 
			
		||||
    args = argparse.ArgumentParser(description = 'Backups manager',
 | 
			
		||||
                                   epilog = 'TIP: this program has context-specific help. e.g. try "%(prog)s list --help"')
 | 
			
		||||
                                   epilog = ('TIP: this program has '
 | 
			
		||||
                                             'context-specific help. e.g. '
 | 
			
		||||
                                             'try "%(prog)s list --help"'))
 | 
			
		||||
    args.add_argument('-c', '--config',
 | 
			
		||||
                      dest = 'cfgfile',
 | 
			
		||||
                      default = _cfgfile,
 | 
			
		||||
                      help = ('The path to the config file. Default: \033[1m{0}\033[0m'.format(_cfgfile)))
 | 
			
		||||
                      help = (
 | 
			
		||||
                          'The path to the config file. '
 | 
			
		||||
                          'Default: \033[1m{0}\033[0m'.format(_cfgfile)))
 | 
			
		||||
    args.add_argument('-Ll', '--loglevel',
 | 
			
		||||
                      dest = 'loglevel',
 | 
			
		||||
                      default = _defloglvl,
 | 
			
		||||
                      choices = list(loglvls.keys()),
 | 
			
		||||
                      help = ('The level of logging to perform. \033[1mWARNING:\033[0m \033[1mdebug\033[0m will log ' +
 | 
			
		||||
                              'VERY sensitive information such as passwords! Default: \033[1m{0}\033[0m'.format(_defloglvl)))
 | 
			
		||||
                      help = (
 | 
			
		||||
                          'The level of logging to perform. '
 | 
			
		||||
                          '\033[1mWARNING:\033[0m \033[1mdebug\033[0m will '
 | 
			
		||||
                          'log VERY sensitive information such as passwords! '
 | 
			
		||||
                          'Default: \033[1m{0}\033[0m'.format(_defloglvl)))
 | 
			
		||||
    args.add_argument('-Ld', '--log-to-disk',
 | 
			
		||||
                      dest = 'disklog',
 | 
			
		||||
                      action = 'store_true',
 | 
			
		||||
                      help = ('If specified, log to a specific file (-Lf/--logfile)' +
 | 
			
		||||
                              ' instead of the system logger.'))
 | 
			
		||||
                      help = (
 | 
			
		||||
                          'If specified, log to a specific file '
 | 
			
		||||
                          '(-Lf/--logfile) instead of the system logger.'))
 | 
			
		||||
    args.add_argument('-Lf', '--logfile',
 | 
			
		||||
                      dest = 'logfile',
 | 
			
		||||
                      default = _logfile,
 | 
			
		||||
                      help = ('The path to the logfile, only used if -Ld/--log-to-disk ' +
 | 
			
		||||
                              'is specified. Default: \033[1m{0}\033[0m (dynamic)').format(_logfile))
 | 
			
		||||
                      help = (
 | 
			
		||||
                          'The path to the logfile, only used if '
 | 
			
		||||
                          '-Ld/--log-to-disk is specified. '
 | 
			
		||||
                          'Default: \033[1m{0}\033[0m (dynamic)').format(
 | 
			
		||||
                              _logfile))
 | 
			
		||||
    args.add_argument('-v', '--verbose',
 | 
			
		||||
                      dest = 'verbose',
 | 
			
		||||
                      action = 'store_true',
 | 
			
		||||
                      help = ('If specified, log messages will be printed to STDERR ' +
 | 
			
		||||
                              'in addition to the other configured log system(s), and verbosity for printing ' +
 | 
			
		||||
                              'functions is increased. \033[1mWARNING:\033[0m This may display VERY sensitive information ' +
 | 
			
		||||
                              'such as passwords!'))
 | 
			
		||||
                      help = (
 | 
			
		||||
                          'If specified, log messages will be printed to '
 | 
			
		||||
                          'STDERR in addition to the other configured log '
 | 
			
		||||
                          'system(s), and verbosity for printing '
 | 
			
		||||
                          'functions is increased. '
 | 
			
		||||
                          '\033[1mWARNING:\033[0m This may display VERY '
 | 
			
		||||
                          'sensitive information such as passwords!'))
 | 
			
		||||
    ### ARGS FOR ALL OPERATIONS ###
 | 
			
		||||
    commonargs = argparse.ArgumentParser(add_help = False)
 | 
			
		||||
    commonargs.add_argument('-r', '--repo',
 | 
			
		||||
                            dest = 'repo',
 | 
			
		||||
                            default = 'all',
 | 
			
		||||
                            help = ('The repository to perform the operation for. ' +
 | 
			
		||||
                                    'The default is \033[1mall\033[0m, a special value that specifies all known ' +
 | 
			
		||||
                                    'repositories. Can also accept a comma-separated list.'))
 | 
			
		||||
                            help = (
 | 
			
		||||
                                'The repository to perform the operation for. '
 | 
			
		||||
                                'The default is \033[1mall\033[0m, a special '
 | 
			
		||||
                                'value that specifies all known repositories. '
 | 
			
		||||
                                'Can also accept a comma-separated list.'))
 | 
			
		||||
    fileargs = argparse.ArgumentParser(add_help = False)
 | 
			
		||||
    fileargs.add_argument('-a', '--archive',
 | 
			
		||||
                          default = _date,
 | 
			
		||||
                          dest = 'archive',
 | 
			
		||||
                          help = (
 | 
			
		||||
                              'The name of the archive/snapshot. '
 | 
			
		||||
                              'Default: \033[1m{0}\033[0m (dynamic)').format(
 | 
			
		||||
                                                                        _date))
 | 
			
		||||
    remoteargs = argparse.ArgumentParser(add_help = False)
 | 
			
		||||
    remoteargs.add_argument('-d', '--dry-run',
 | 
			
		||||
                            dest = 'dryrun',
 | 
			
		||||
                            action = 'store_true',
 | 
			
		||||
                            help = ('Act as if we are performing tasks, but none will actually be executed ' +
 | 
			
		||||
                            help = ('Act as if we are performing tasks, but '
 | 
			
		||||
                                    'none will actually be executed '
 | 
			
		||||
                                    '(useful for testing logging)'))
 | 
			
		||||
    ### OPERATIONS ###
 | 
			
		||||
    subparsers = args.add_subparsers(help = 'Operation to perform',
 | 
			
		||||
                                     dest = 'oper')
 | 
			
		||||
    backupargs = subparsers.add_parser('backup',
 | 
			
		||||
                                       help = 'Perform a backup.',
 | 
			
		||||
                                       parents = [commonargs, remoteargs])
 | 
			
		||||
                                       parents = [commonargs,
 | 
			
		||||
                                                  remoteargs,
 | 
			
		||||
                                                  fileargs])
 | 
			
		||||
    listargs = subparsers.add_parser('list',
 | 
			
		||||
                                     help = 'List available backups.',
 | 
			
		||||
                                     parents = [commonargs, remoteargs])
 | 
			
		||||
    listrepoargs = subparsers.add_parser('listrepos',
 | 
			
		||||
                                         help = 'List availabile/configured repositories.',
 | 
			
		||||
                                         help = ('List availabile/configured '
 | 
			
		||||
                                                 'repositories.'),
 | 
			
		||||
                                         parents = [commonargs])
 | 
			
		||||
    initargs = subparsers.add_parser('init',
 | 
			
		||||
                                     help = 'Initialise a repository.',
 | 
			
		||||
                                     parents = [commonargs, remoteargs])
 | 
			
		||||
    rstrargs = subparsers.add_parser('restore',
 | 
			
		||||
                                     help = ('Restore ("extract") an '
 | 
			
		||||
                                             'archive.'),
 | 
			
		||||
                                     parents = [commonargs,
 | 
			
		||||
                                                remoteargs,
 | 
			
		||||
                                                fileargs])
 | 
			
		||||
    ### OPERATION-SPECIFIC OPTIONS ###
 | 
			
		||||
    # CREATE ("backup") #
 | 
			
		||||
    backupargs.add_argument('-a',
 | 
			
		||||
                            '--archive',
 | 
			
		||||
                            default = _date,
 | 
			
		||||
                            dest = 'archive',
 | 
			
		||||
                            help = ('The name of the archive. Default: \033[1m{0}\033[0m (dynamic)').format(_date))
 | 
			
		||||
    backupargs.add_argument('-s',
 | 
			
		||||
                            '--stagedir',
 | 
			
		||||
    backupargs.add_argument('-s', '--stagedir',
 | 
			
		||||
                            default = _stagedir,
 | 
			
		||||
                            dest = 'stagedir',
 | 
			
		||||
                            help = ('The directory used for staging temporary files, ' +
 | 
			
		||||
                                    'if necessary. Default: \033[1m{0}\033[0m').format(_stagedir))
 | 
			
		||||
    backupargs.add_argument('-m',
 | 
			
		||||
                            '--mysqldir',
 | 
			
		||||
                            help = (
 | 
			
		||||
                                    'The directory used for staging '
 | 
			
		||||
                                    'temporary files, if necessary. '
 | 
			
		||||
                                    'Default: \033[1m{0}\033[0m').format(
 | 
			
		||||
                                                                    _stagedir))
 | 
			
		||||
    backupargs.add_argument('-m', '--mysqldir',
 | 
			
		||||
                            default = _mysqldir,
 | 
			
		||||
                            dest = 'mysqldir',
 | 
			
		||||
                            help = ('The path to where MySQL dumps should go. ' +
 | 
			
		||||
                                    'Default: \033[1m{0}\033[0m').format(_mysqldir))
 | 
			
		||||
                            help = (
 | 
			
		||||
                                    'The path to where MySQL dumps should go. '
 | 
			
		||||
                                    'Default: \033[1m{0}\033[0m').format(
 | 
			
		||||
                                                                    _mysqldir))
 | 
			
		||||
    # DISPLAY/OUTPUT ("list") #
 | 
			
		||||
    listargs.add_argument('-a',
 | 
			
		||||
                          '--archive',
 | 
			
		||||
    listargs.add_argument('-a', '--archive',
 | 
			
		||||
                          dest = 'archive',
 | 
			
		||||
                          default = False,
 | 
			
		||||
                          help = 'If specified, will list the *contents* of the given archive name.')
 | 
			
		||||
    listargs.add_argument('-l',
 | 
			
		||||
                          '--limit',
 | 
			
		||||
                          help = 'If specified, will list the *contents* of '
 | 
			
		||||
                                 'the given archive name.')
 | 
			
		||||
    listargs.add_argument('-l', '--limit',
 | 
			
		||||
                          dest = 'numlimit',
 | 
			
		||||
                          type = int,
 | 
			
		||||
                          default = '5',
 | 
			
		||||
                          help = ('If specified, constrain the outout to this number of ' +
 | 
			
		||||
                                  'results each repo. Default is \033[1m5\033[0m, use 0 for unlimited. ' +
 | 
			
		||||
                                  'See \033[1m-H/--list-help\033[0m'))
 | 
			
		||||
    listargs.add_argument('-s',
 | 
			
		||||
                          '--sort',
 | 
			
		||||
                          help = (
 | 
			
		||||
                                  'If specified, constrain the outout to this '
 | 
			
		||||
                                  'number of results each repo. '
 | 
			
		||||
                                  'Default is \033[1m5\033[0m, use 0 for '
 | 
			
		||||
                                  'unlimited. See '
 | 
			
		||||
                                  '\033[1m-H/--list-help\033[0m'))
 | 
			
		||||
    listargs.add_argument('-s', '--sort',
 | 
			
		||||
                          dest = 'sortby',
 | 
			
		||||
                          choices = ['newest', 'oldest'],
 | 
			
		||||
                          default = 'oldest',
 | 
			
		||||
                          help = ('The order to sort the results by. See \033[1m-H/--list-help\033[0m. ' +
 | 
			
		||||
                          help = (
 | 
			
		||||
                                  'The order to sort the results by. '
 | 
			
		||||
                                  'See \033[1m-H/--list-help\033[0m. '
 | 
			
		||||
                                  'Default: \033[1moldest\033[0m'))
 | 
			
		||||
    listargs.add_argument('-x',
 | 
			
		||||
                          '--invert',
 | 
			
		||||
    listargs.add_argument('-x', '--invert',
 | 
			
		||||
                          dest = 'invert',
 | 
			
		||||
                          action = 'store_true',
 | 
			
		||||
                          help = 'Invert the order of results. See \033[1m-H/--list-help\033[0m.')
 | 
			
		||||
    listargs.add_argument('-o',
 | 
			
		||||
                          '--old',
 | 
			
		||||
                          help = 'Invert the order of results. '
 | 
			
		||||
                                 'See \033[1m-H/--list-help\033[0m.')
 | 
			
		||||
    listargs.add_argument('-o', '--old',
 | 
			
		||||
                          dest = 'old',
 | 
			
		||||
                          action = 'store_true',
 | 
			
		||||
                          help = ('Instead of grabbing the latest results, grab the earliest results. ' +
 | 
			
		||||
                                  'This differs from \033[1m-s/--sort\033[0m. See \033[1m-H/--list-help\033[0m.'))
 | 
			
		||||
    listargs.add_argument('-H',
 | 
			
		||||
                          '--list-help',
 | 
			
		||||
                          help = (
 | 
			
		||||
                                  'Instead of grabbing the latest results, '
 | 
			
		||||
                                  'grab the earliest results. '
 | 
			
		||||
                                  'This differs from \033[1m-s/--sort\033[0m. '
 | 
			
		||||
                                  'See \033[1m-H/--list-help\033[0m.'))
 | 
			
		||||
    listargs.add_argument('-H', '--list-help',
 | 
			
		||||
                          dest = 'moarhelp',
 | 
			
		||||
                          action = 'store_true',
 | 
			
		||||
                          help = 'Print extended information about how to manage the output of listing and exit.')
 | 
			
		||||
    return(args)
 | 
			
		||||
                          help = ('Print extended information about how to '
 | 
			
		||||
                                  'manage the output of listing and exit.'))
 | 
			
		||||
    ## EXTRACT ("restore")
 | 
			
		||||
    rstrargs.add_argument('-t', '--target',
 | 
			
		||||
                          required = True,
 | 
			
		||||
                          dest = 'target_dir',
 | 
			
		||||
                          help = ('The path to the directory where the '
 | 
			
		||||
                                  'restore should be dumped to. It is '
 | 
			
		||||
                                  'recommended to NOT restore to the same '
 | 
			
		||||
                                  'directory that the archive is taken from.'))
 | 
			
		||||
    return (args)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
    rawargs = parseArgs()
 | 
			
		||||
    args = vars(rawargs.parse_args())
 | 
			
		||||
    parsedargs = rawargs.parse_args()
 | 
			
		||||
    args = vars(parsedargs)
 | 
			
		||||
    args['cfgfile'] = os.path.abspath(os.path.expanduser(args['cfgfile']))
 | 
			
		||||
    if not args['oper']:
 | 
			
		||||
        rawargs.print_help()
 | 
			
		||||
@ -543,7 +711,10 @@ def main():
 | 
			
		||||
        bak.create()
 | 
			
		||||
    elif args['oper'] == 'init':
 | 
			
		||||
        bak.createRepo()
 | 
			
		||||
    return()
 | 
			
		||||
    
 | 
			
		||||
    elif args['oper'] == 'restore':
 | 
			
		||||
        bak.restore()
 | 
			
		||||
    return ()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    main()
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user