adding restore functionality

This commit is contained in:
brent s 2018-08-07 17:42:54 -04:00
parent b566970d57
commit 120b576a38
1 changed files with 298 additions and 127 deletions

View File

@ -3,6 +3,8 @@
# TODO: https://borgbackup.readthedocs.io/en/latest/internals/frontends.html # 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 # will they EVER release a public API? for now we'll just use subprocess since
# we import it for various prep stuff anyways. # 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 argparse
import configparser import configparser
@ -13,6 +15,7 @@ import logging.handlers
import os import os
import subprocess import subprocess
import sys import sys

try: try:
import pymysql # not stdlib; "python-pymysql" in Arch's AUR import pymysql # not stdlib; "python-pymysql" in Arch's AUR
has_mysql = True has_mysql = True
@ -26,12 +29,14 @@ except ImportError:
has_systemd = False has_systemd = False


### LOG LEVEL MAPPINGS ### ### LOG LEVEL MAPPINGS ###
loglvls = {'critical': logging.CRITICAL, loglvls = {
'critical': logging.CRITICAL,
'error': logging.ERROR, 'error': logging.ERROR,
'warning': logging.WARNING, 'warning': logging.WARNING,
'info': logging.INFO, 'info': logging.INFO,
'debug': logging.DEBUG} 'debug': logging.DEBUG}



### THE GUTS ### ### THE GUTS ###
class Backup(object): class Backup(object):
def __init__(self, args): def __init__(self, args):
@ -48,10 +53,13 @@ class Backup(object):
# and user K900_ on r/python for entertaining my very silly question. # and user K900_ on r/python for entertaining my very silly question.
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.logger.setLevel(loglvls[self.args['loglevel']]) self.logger.setLevel(loglvls[self.args['loglevel']])
_logfmt = logging.Formatter(fmt = '{levelname}:{name}: {message} ({asctime}; {filename}:{lineno})', _logfmt = logging.Formatter(
fmt = ('{levelname}:{name}: {message} ({asctime}; '
'{filename}:{lineno})'),
style = '{', style = '{',
datefmt = '%Y-%m-%d %H:%M:%S') datefmt = '%Y-%m-%d %H:%M:%S')
_journalfmt = logging.Formatter(fmt = '{levelname}:{name}: {message} ({filename}:{lineno})', _journalfmt = logging.Formatter(
fmt = '{levelname}:{name}: {message} ({filename}:{lineno})',
style = '{', style = '{',
datefmt = '%Y-%m-%d %H:%M:%S') datefmt = '%Y-%m-%d %H:%M:%S')
handlers = [] handlers = []
@ -59,7 +67,9 @@ class Backup(object):
os.makedirs(os.path.dirname(self.args['logfile']), os.makedirs(os.path.dirname(self.args['logfile']),
exist_ok = True, exist_ok = True,
mode = 0o700) mode = 0o700)
handlers.append(logging.handlers.RotatingFileHandler(self.args['logfile'], # TODO: make the constraints for rotation in config?
handlers.append(
logging.handlers.RotatingFileHandler(self.args['logfile'],
encoding = 'utf8', encoding = 'utf8',
maxBytes = 100000, maxBytes = 100000,
backupCount = 1)) backupCount = 1))
@ -77,7 +87,8 @@ class Backup(object):
self.logger.debug('BEGIN INITIALIZATION') self.logger.debug('BEGIN INITIALIZATION')
### CONFIG ### ### CONFIG ###
if not os.path.isfile(self.args['cfgfile']): 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) exit(1)
with open(self.args['cfgfile'], 'r') as f: with open(self.args['cfgfile'], 'r') as f:
self.cfg = json.loads(f.read()) self.cfg = json.loads(f.read())
@ -91,7 +102,9 @@ class Backup(object):
if r == 'all': if r == 'all':
self.args['repo'].remove(r) self.args['repo'].remove(r)
elif r not in self.cfg['repos'].keys(): 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.args['repo'].remove(r)
self.logger.debug('VARS (after args cleanup): {0}'.format(vars())) self.logger.debug('VARS (after args cleanup): {0}'.format(vars()))
self.logger.debug('END INITIALIZATION') self.logger.debug('END INITIALIZATION')
@ -108,7 +121,8 @@ class Backup(object):
if self.args['dryrun']: if self.args['dryrun']:
return () # no-op return () # no-op
if stdoutfh: if stdoutfh:
_cmd = subprocess.run(cmd, stdout = stdoutfh, stderr = subprocess.PIPE) _cmd = subprocess.run(cmd, stdout = stdoutfh,
stderr = subprocess.PIPE)
else: else:
_cmd = subprocess.run(cmd, _cmd = subprocess.run(cmd,
stdout = subprocess.PIPE, stdout = subprocess.PIPE,
@ -119,7 +133,8 @@ class Backup(object):
if _returncode != 0: if _returncode != 0:
self.logger.error('STDERR: ({1})\n{0}'.format(_err, ' '.join(cmd))) self.logger.error('STDERR: ({1})\n{0}'.format(_err, ' '.join(cmd)))
if _err != '' and self.cron: 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() return()


def createRepo(self): def createRepo(self):
@ -149,16 +164,21 @@ class Backup(object):
# sigh. borg uses stderr for verbose output. # sigh. borg uses stderr for verbose output.
self.logger.debug('[{0}]: STDERR: ({2})\n{1}'.format(r, self.logger.debug('[{0}]: STDERR: ({2})\n{1}'.format(r,
_stderr, _stderr,
' '.join(_cmd))) ' '.join(
_cmd)))
if _returncode != 0: 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: if _err != '' and self.cron and _returncode != 0:
self.logger.warning('Command {0} failed: {1}'.format(' '.join(cmd), _err)) self.logger.warning(
'Command {0} failed: {1}'.format(' '.join(cmd),
_err))
del (_env['BORG_PASSPHRASE']) del (_env['BORG_PASSPHRASE'])
self.logger.info('[{0}]: END INITIALIZATION'.format(r)) self.logger.info('[{0}]: END INITIALIZATION'.format(r))
return() return()


def create(self): def create(self):
# TODO: support "--strip-components N"?
_env = os.environ.copy() _env = os.environ.copy()
_env['BORG_RSH'] = self.cfg['config']['ctx'] _env['BORG_RSH'] = self.cfg['config']['ctx']
self.logger.info('START: backup') self.logger.info('START: backup')
@ -166,9 +186,12 @@ class Backup(object):
self.logger.info('[{0}]: BEGIN BACKUP'.format(r)) self.logger.info('[{0}]: BEGIN BACKUP'.format(r))
if 'prep' in self.cfg['repos'][r].keys(): if 'prep' in self.cfg['repos'][r].keys():
for prep in self.cfg['repos'][r]['prep']: for prep in self.cfg['repos'][r]['prep']:
self.logger.info('[{0}]: Running prepfunc {1}'.format(r, prep)) self.logger.info(
eval('self.{0}'.format(prep)) # I KNOW, IT'S TERRIBLE. so sue me. '[{0}]: Running prepfunc {1}'.format(r, prep))
self.logger.info('[{0}]: Finished 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', _cmd = ['borg',
'create', 'create',
'-v', '--stats', '-v', '--stats',
@ -184,9 +207,10 @@ class Backup(object):
_cmd.append(p) _cmd.append(p)
_env['BORG_PASSPHRASE'] = self.cfg['repos'][r]['password'] _env['BORG_PASSPHRASE'] = self.cfg['repos'][r]['password']
self.logger.debug('VARS: {0}'.format(vars())) self.logger.debug('VARS: {0}'.format(vars()))
# We don't use self.cmdExec() here because we want to explicitly pass the env # We don't use self.cmdExec() here because we want to explicitly
# and format the log line differently. # pass the env and format the log line differently.
self.logger.debug('[{0}]: Running command: {1}'.format(r, ' '.join(_cmd))) self.logger.debug(
'[{0}]: Running command: {1}'.format(r, ' '.join(_cmd)))
if not self.args['dryrun']: if not self.args['dryrun']:
_out = subprocess.run(_cmd, _out = subprocess.run(_cmd,
env = _env, env = _env,
@ -198,16 +222,73 @@ class Backup(object):
self.logger.debug('[{0}]: (RESULT) {1}'.format(r, _stdout)) self.logger.debug('[{0}]: (RESULT) {1}'.format(r, _stdout))
self.logger.error('[{0}]: STDERR: ({2})\n{1}'.format(r, self.logger.error('[{0}]: STDERR: ({2})\n{1}'.format(r,
_stderr, _stderr,
' '.join(_cmd))) ' '.join(
_cmd)))
if _returncode != 0: 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: if _stderr != '' and self.cron and _returncode != 0:
self.logger.warning('Command {0} failed: {1}'.format(' '.join(_cmd), _stderr)) self.logger.warning(
'Command {0} failed: {1}'.format(' '.join(_cmd),
_stderr))
del (_env['BORG_PASSPHRASE']) del (_env['BORG_PASSPHRASE'])
self.logger.info('[{0}]: END BACKUP'.format(r)) self.logger.info('[{0}]: END BACKUP'.format(r))
self.logger.info('END: backup') self.logger.info('END: backup')
return() 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): def miscBak(self, pkgr):
self.logger.info('BEGIN: miscBak()') self.logger.info('BEGIN: miscBak()')
_cmd = None _cmd = None
@ -231,7 +312,8 @@ class Backup(object):
def mysqlBak(self): def mysqlBak(self):
self.logger.info('BEGIN: mysqlBak()') self.logger.info('BEGIN: mysqlBak()')
if not has_mysql: if not has_mysql:
self.logger.error('You need to install the PyMySQL module to back up MySQL databases. Skipping.') self.logger.error(
'You need to install the PyMySQL module to back up MySQL databases. Skipping.')
return () return ()
# These are mysqldump options shared by ALL databases # These are mysqldump options shared by ALL databases
_mysqlopts = ['--routines', _mysqlopts = ['--routines',
@ -244,7 +326,8 @@ class Backup(object):
_DBs = [] _DBs = []
_mycnf = os.path.expanduser(os.path.join('~', '.my.cnf')) _mycnf = os.path.expanduser(os.path.join('~', '.my.cnf'))
if not os.path.isfile(_mycnf): 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 = configparser.ConfigParser()
_mycfg._interpolation = configparser.ExtendedInterpolation() _mycfg._interpolation = configparser.ExtendedInterpolation()
_mycfg.read(_mycnf) _mycfg.read(_mycnf)
@ -266,7 +349,8 @@ class Backup(object):
self.logger.debug('Databases: {0}'.format(', '.join(_DBs))) self.logger.debug('Databases: {0}'.format(', '.join(_DBs)))
for db in _DBs: for db in _DBs:
_cmd = ['mysqldump', _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 # These are database-specific options
if db in ('information_schema', 'performance_schema'): if db in ('information_schema', 'performance_schema'):
_cmd.append('--skip-lock-tables') _cmd.append('--skip-lock-tables')
@ -284,7 +368,9 @@ class Backup(object):
if self.args['verbose']: if self.args['verbose']:
print('\033[1mDETAILS:\033[0m\n') print('\033[1mDETAILS:\033[0m\n')
for r in self.args['repo']: 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']: for p in self.cfg['repos'][r]['paths']:
print(p, end = ' ') print(p, end = ' ')
if 'prep' in self.cfg['repos'][r].keys(): if 'prep' in self.cfg['repos'][r].keys():
@ -307,15 +393,19 @@ class Backup(object):
print(r, end = '') print(r, end = '')
for line in _results[r]: for line in _results[r]:
_snapshot = line.split() _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() print()
else: # It's a listing inside an archive else: # It's a listing inside an archive
if self.args['verbose']: if self.args['verbose']:
_fields = ['REPO:', 'PERMS:', 'OWNERSHIP:', 'SIZE:', 'TIMESTAMP:', 'PATH:'] _fields = ['REPO:', 'PERMS:', 'OWNERSHIP:', 'SIZE:',
'TIMESTAMP:', 'PATH:']
for r in _results.keys(): for r in _results.keys():
print('\033[1m{0}\t{1}\033[0m'.format(_fields[0], r)) print('\033[1m{0}\t{1}\033[0m'.format(_fields[0], r))
# https://docs.python.org/3/library/string.html#formatspec # 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]: for line in _results[r]:
_fline = line.split() _fline = line.split()
_perms = _fline[0] _perms = _fline[0]
@ -323,7 +413,9 @@ class Backup(object):
_size = _fline[3] _size = _fline[3]
_time = ' '.join(_fline[4:7]) _time = ' '.join(_fline[4:7])
_path = ' '.join(_fline[7:]) _path = ' '.join(_fline[7:])
print('{0:<15}\t{1:<15}\t{2:<15}\t{3:<24}\t{4:<15}'.format(_perms, print(
'{0:<15}\t{1:<15}\t{2:<15}\t{3:<24}\t{4:<15}'.format(
_perms,
_ownership, _ownership,
_size, _size,
_time, _time,
@ -363,170 +455,246 @@ class Backup(object):
env = _env, env = _env,
stdout = subprocess.PIPE, stdout = subprocess.PIPE,
stderr = subprocess.PIPE) stderr = subprocess.PIPE)
_stdout = [i.strip() for i in _out.stdout.decode('utf-8').splitlines()] _stdout = [i.strip() for i in
_out.stdout.decode('utf-8').splitlines()]
_stderr = _out.stderr.decode('utf-8').strip() _stderr = _out.stderr.decode('utf-8').strip()
_returncode = _out.returncode _returncode = _out.returncode
output[r] = _stdout output[r] = _stdout
self.logger.debug('[{0}]: (RESULT) {1}'.format(r, self.logger.debug('[{0}]: (RESULT) {1}'.format(r,
'\n'.join(_stdout))) '\n'.join(
_stdout)))
if _returncode != 0: if _returncode != 0:
self.logger.error('[{0}]: STDERR: ({2}) ({1})'.format(r, self.logger.error('[{0}]: STDERR: ({2}) ({1})'.format(r,
_stderr, _stderr,
' '.join(_cmd))) ' '.join(
_cmd)))
if _stderr != '' and self.cron and _returncode != 0: if _stderr != '' and self.cron and _returncode != 0:
self.logger.warning('Command {0} failed: {1}'.format(' '.join(cmd), _err)) self.logger.warning(
'Command {0} failed: {1}'.format(' '.join(cmd),
_err))
del (_env['BORG_PASSPHRASE']) del (_env['BORG_PASSPHRASE'])
if not self.args['archive']: if not self.args['archive']:
if self.args['numlimit'] > 0: if self.args['numlimit'] > 0:
if self.args['old']: if self.args['old']:
output[r] = output[r][:self.args['numlimit']] output[r] = output[r][:self.args['numlimit']]
else: else:
output[r] = list(reversed(output[r]))[:self.args['numlimit']] output[r] = list(reversed(output[r]))[
:self.args['numlimit']]
if self.args['invert']: if self.args['invert']:
output[r] = reversed(output[r]) output[r] = reversed(output[r])
self.logger.debug('END: lister') self.logger.debug('END: lister')
return(output) return(output)



def printMoarHelp(): def printMoarHelp():
_helpstr = ('\n\tNOTE: Sorting only applies to listing archives, NOT the contents!\n\n' + _helpstr = (
'In order to efficiently display results, there are several options to handle it. ' + '\n\tNOTE: Sorting only applies to listing archives, NOT the '
'Namely, these are:\n\n\t\t-s/--sort [direction]\n\t\t-l/--limit [number]\n\t\t-x/--invert\n\n' + 'contents!\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' + 'In order to efficiently display results, there are several options '
'If you would want those SAME results SORTED in the reverse order (i.e. the 5 most recently ' + 'to handle it. Namely, these are:\n\n\t\t'
'taken snapshots sorted from newest to oldest), then it would be: \n\n\t\t-l 5 -x\n\n' + '-s/--sort [direction]\n\t\t'
'Lastly, if you wanted to list the 7 OLDEST TAKEN snapshots in reverse order (that is, ' + '-l/--limit [number]\n\t\t'
'sorted from newest to oldest), that\'d be: \n\n\t\t-o -l 7 -x\n') '-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) print(_helpstr)
exit(0) exit(0)



def parseArgs(): def parseArgs():
### DEFAULTS ### ### DEFAULTS ###
_date = datetime.datetime.now().strftime("%Y_%m_%d.%H_%M") _date = datetime.datetime.now().strftime("%Y_%m_%d.%H_%M")
_logfile = '/var/log/borg/{0}'.format(_date) _logfile = '/var/log/borg/{0}'.format(_date)
_mysqldir = os.path.abspath(os.path.join(os.path.expanduser('~'), '.bak', 'mysql')) _mysqldir = os.path.abspath(
_stagedir = os.path.abspath(os.path.join(os.path.expanduser('~'), '.bak', 'misc')) os.path.join(os.path.expanduser('~'),
_cfgfile = os.path.abspath(os.path.join(os.path.expanduser('~'), '.config', 'optools', 'backup.json')) '.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' _defloglvl = 'info'
###### ######
args = argparse.ArgumentParser(description = 'Backups manager', 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', args.add_argument('-c', '--config',
dest = 'cfgfile', dest = 'cfgfile',
default = _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', args.add_argument('-Ll', '--loglevel',
dest = 'loglevel', dest = 'loglevel',
default = _defloglvl, default = _defloglvl,
choices = list(loglvls.keys()), choices = list(loglvls.keys()),
help = ('The level of logging to perform. \033[1mWARNING:\033[0m \033[1mdebug\033[0m will log ' + help = (
'VERY sensitive information such as passwords! Default: \033[1m{0}\033[0m'.format(_defloglvl))) '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', args.add_argument('-Ld', '--log-to-disk',
dest = 'disklog', dest = 'disklog',
action = 'store_true', action = 'store_true',
help = ('If specified, log to a specific file (-Lf/--logfile)' + help = (
' instead of the system logger.')) 'If specified, log to a specific file '
'(-Lf/--logfile) instead of the system logger.'))
args.add_argument('-Lf', '--logfile', args.add_argument('-Lf', '--logfile',
dest = 'logfile', dest = 'logfile',
default = _logfile, default = _logfile,
help = ('The path to the logfile, only used if -Ld/--log-to-disk ' + help = (
'is specified. Default: \033[1m{0}\033[0m (dynamic)').format(_logfile)) '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', args.add_argument('-v', '--verbose',
dest = 'verbose', dest = 'verbose',
action = 'store_true', action = 'store_true',
help = ('If specified, log messages will be printed to STDERR ' + help = (
'in addition to the other configured log system(s), and verbosity for printing ' + 'If specified, log messages will be printed to '
'functions is increased. \033[1mWARNING:\033[0m This may display VERY sensitive information ' + 'STDERR in addition to the other configured log '
'such as passwords!')) '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 ### ### ARGS FOR ALL OPERATIONS ###
commonargs = argparse.ArgumentParser(add_help = False) commonargs = argparse.ArgumentParser(add_help = False)
commonargs.add_argument('-r', '--repo', commonargs.add_argument('-r', '--repo',
dest = 'repo', dest = 'repo',
default = 'all', default = 'all',
help = ('The repository to perform the operation for. ' + help = (
'The default is \033[1mall\033[0m, a special value that specifies all known ' + 'The repository to perform the operation for. '
'repositories. Can also accept a comma-separated list.')) '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 = argparse.ArgumentParser(add_help = False)
remoteargs.add_argument('-d', '--dry-run', remoteargs.add_argument('-d', '--dry-run',
dest = 'dryrun', dest = 'dryrun',
action = 'store_true', 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)')) '(useful for testing logging)'))
### OPERATIONS ### ### OPERATIONS ###
subparsers = args.add_subparsers(help = 'Operation to perform', subparsers = args.add_subparsers(help = 'Operation to perform',
dest = 'oper') dest = 'oper')
backupargs = subparsers.add_parser('backup', backupargs = subparsers.add_parser('backup',
help = 'Perform a backup.', help = 'Perform a backup.',
parents = [commonargs, remoteargs]) parents = [commonargs,
remoteargs,
fileargs])
listargs = subparsers.add_parser('list', listargs = subparsers.add_parser('list',
help = 'List available backups.', help = 'List available backups.',
parents = [commonargs, remoteargs]) parents = [commonargs, remoteargs])
listrepoargs = subparsers.add_parser('listrepos', listrepoargs = subparsers.add_parser('listrepos',
help = 'List availabile/configured repositories.', help = ('List availabile/configured '
'repositories.'),
parents = [commonargs]) parents = [commonargs])
initargs = subparsers.add_parser('init', initargs = subparsers.add_parser('init',
help = 'Initialise a repository.', help = 'Initialise a repository.',
parents = [commonargs, remoteargs]) parents = [commonargs, remoteargs])
rstrargs = subparsers.add_parser('restore',
help = ('Restore ("extract") an '
'archive.'),
parents = [commonargs,
remoteargs,
fileargs])
### OPERATION-SPECIFIC OPTIONS ### ### OPERATION-SPECIFIC OPTIONS ###
# CREATE ("backup") # # CREATE ("backup") #
backupargs.add_argument('-a', backupargs.add_argument('-s', '--stagedir',
'--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',
default = _stagedir, default = _stagedir,
dest = 'stagedir', dest = 'stagedir',
help = ('The directory used for staging temporary files, ' + help = (
'if necessary. Default: \033[1m{0}\033[0m').format(_stagedir)) 'The directory used for staging '
backupargs.add_argument('-m', 'temporary files, if necessary. '
'--mysqldir', 'Default: \033[1m{0}\033[0m').format(
_stagedir))
backupargs.add_argument('-m', '--mysqldir',
default = _mysqldir, default = _mysqldir,
dest = 'mysqldir', dest = 'mysqldir',
help = ('The path to where MySQL dumps should go. ' + help = (
'Default: \033[1m{0}\033[0m').format(_mysqldir)) 'The path to where MySQL dumps should go. '
'Default: \033[1m{0}\033[0m').format(
_mysqldir))
# DISPLAY/OUTPUT ("list") # # DISPLAY/OUTPUT ("list") #
listargs.add_argument('-a', listargs.add_argument('-a', '--archive',
'--archive',
dest = 'archive', dest = 'archive',
default = False, default = False,
help = 'If specified, will list the *contents* of the given archive name.') help = 'If specified, will list the *contents* of '
listargs.add_argument('-l', 'the given archive name.')
'--limit', listargs.add_argument('-l', '--limit',
dest = 'numlimit', dest = 'numlimit',
type = int, type = int,
default = '5', default = '5',
help = ('If specified, constrain the outout to this number of ' + help = (
'results each repo. Default is \033[1m5\033[0m, use 0 for unlimited. ' + 'If specified, constrain the outout to this '
'See \033[1m-H/--list-help\033[0m')) 'number of results each repo. '
listargs.add_argument('-s', 'Default is \033[1m5\033[0m, use 0 for '
'--sort', 'unlimited. See '
'\033[1m-H/--list-help\033[0m'))
listargs.add_argument('-s', '--sort',
dest = 'sortby', dest = 'sortby',
choices = ['newest', 'oldest'], choices = ['newest', 'oldest'],
default = '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')) 'Default: \033[1moldest\033[0m'))
listargs.add_argument('-x', listargs.add_argument('-x', '--invert',
'--invert',
dest = 'invert', dest = 'invert',
action = 'store_true', action = 'store_true',
help = 'Invert the order of results. See \033[1m-H/--list-help\033[0m.') help = 'Invert the order of results. '
listargs.add_argument('-o', 'See \033[1m-H/--list-help\033[0m.')
'--old', listargs.add_argument('-o', '--old',
dest = 'old', dest = 'old',
action = 'store_true', action = 'store_true',
help = ('Instead of grabbing the latest results, grab the earliest results. ' + help = (
'This differs from \033[1m-s/--sort\033[0m. See \033[1m-H/--list-help\033[0m.')) 'Instead of grabbing the latest results, '
listargs.add_argument('-H', 'grab the earliest results. '
'--list-help', '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', dest = 'moarhelp',
action = 'store_true', action = 'store_true',
help = 'Print extended information about how to manage the output of listing and exit.') 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) return (args)



def main(): def main():
rawargs = parseArgs() rawargs = parseArgs()
args = vars(rawargs.parse_args()) parsedargs = rawargs.parse_args()
args = vars(parsedargs)
args['cfgfile'] = os.path.abspath(os.path.expanduser(args['cfgfile'])) args['cfgfile'] = os.path.abspath(os.path.expanduser(args['cfgfile']))
if not args['oper']: if not args['oper']:
rawargs.print_help() rawargs.print_help()
@ -543,7 +711,10 @@ def main():
bak.create() bak.create()
elif args['oper'] == 'init': elif args['oper'] == 'init':
bak.createRepo() bak.createRepo()
elif args['oper'] == 'restore':
bak.restore()
return () return ()



if __name__ == '__main__': if __name__ == '__main__':
main() main()