From 120b576a386ba67108ea5765152de5432ac0917a Mon Sep 17 00:00:00 2001 From: brent s Date: Tue, 7 Aug 2018 17:42:54 -0400 Subject: [PATCH] adding restore functionality --- storage/backups/borg/backup.py | 425 +++++++++++++++++++++++---------- 1 file changed, 298 insertions(+), 127 deletions(-) diff --git a/storage/backups/borg/backup.py b/storage/backups/borg/backup.py index 98571d9..e3d8ca5 100755 --- a/storage/backups/borg/backup.py +++ b/storage/backups/borg/backup.py @@ -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()