From 9890b671f2e7656a183492bc1cddf3da0c4850f4 Mon Sep 17 00:00:00 2001 From: Rob Gibson Date: Sat, 21 May 2022 06:13:08 +0000 Subject: [PATCH] Added XSD Support for Pruning Old Backups --- .gitignore | 9 ++++++ backup.py | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ config.xml | 34 +++++++++++++++++++++++ config.xsd | 14 ++++++++++ 4 files changed, 138 insertions(+) create mode 100644 config.xml diff --git a/.gitignore b/.gitignore index ff3b78a..fd69eda 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,12 @@ __pycache__/ *.sqlite3 *.deb .idea/ +files/* +repo/* +repo\:testrepo/* +config +hints.21 +index.21 +integrity.21 +nonce +README \ No newline at end of file diff --git a/backup.py b/backup.py index 7c6b934..f14dc79 100755 --- a/backup.py +++ b/backup.py @@ -20,6 +20,7 @@ import re import subprocess import sys import tempfile +from xmlrpc.client import Server # TODO: virtual env? from lxml import etree # A lot safer and easier to use than the stdlib xml module. try: @@ -452,6 +453,79 @@ class Backup(object): objPrinter(r, indent = 3) print() return() + + def prune(self): + # TODO: support "--strip-components N"? + self.logger.info('START: prune') + for server in self.repos: + _env = os.environ.copy() + if self.repos[server]['remote'].lower()[0] in ('1', 't'): + _env['BORG_RSH'] = self.repos[server].get('rsh', None) + _env['LANG'] = 'en_US.UTF-8' + _env['LC_CTYPE'] = 'en_US.UTF-8' + _user = self.repos[server].get('user', pwd.getpwuid(os.geteuid()).pw_name) + for repo in self.repos[server]['repos']: + _loc_env = _env.copy() + if 'password' not in repo: + print('Password not supplied for {0}:{1}.'.format(server, repo['name'])) + _loc_env['BORG_PASSPHRASE'] = getpass.getpass('Password (will NOT echo back): ') + else: + _loc_env['BORG_PASSPHRASE'] = repo['password'] + self.logger.info('[{0}]: BEGIN PRUNE: {1}'.format(server, repo['name'])) + # This is where we actually do the thing. + _cmd = [self.borgbin, + '--log-json', + '--{0}'.format(self.args['loglevel']), + 'prune', + '--stats'] + if self.repos[server]['keepYearly'][0].isnumeric() and int(self.repos[server]['keepYearly'][0]) > 0: + _cmd.extend(['--keep-yearly', self.repos[server]['keepYearly'].lower()[0]]) + if self.repos[server]['keepMonthly'][0].isnumeric() and int(self.repos[server]['keepMonthly'][0]) > 0: + _cmd.extend(['--keep-monthly', self.repos[server]['keepMonthly'].lower()[0]]) + if self.repos[server]['keepWeekly'][0].isnumeric() and int(self.repos[server]['keepWeekly'][0]) > 0: + _cmd.extend(['--keep-weekly', self.repos[server]['keepWeekly'].lower()[0]]) + if self.repos[server]['keepDaily'][0].isnumeric() and int(self.repos[server]['keepDaily'][0]) > 0: + _cmd.extend(['--keep-daily', self.repos[server]['keepDaily'].lower()[0]]) + if self.repos[server]['keepHourly'][0].isnumeric() and int(self.repos[server]['keepHourly'][0]) > 0: + _cmd.extend(['--keep-hourly', self.repos[server]['keepHourly'].lower()[0]]) + if self.repos[server]['keepMinutely'][0].isnumeric() and int(self.repos[server]['keepMinutely'][0]) > 0: + _cmd.extend(['--keep-minutely', self.repos[server]['keepMinutely'].lower()[0]]) + if self.repos[server]['keepSecondly'][0].isnumeric() and int(self.repos[server]['keepSecondly'][0]) > 0: + _cmd.extend(['--keep-secondly', self.repos[server]['keepSecondly'].lower()[0]]) + if self.repos[server]['remote'].lower()[0] in ('1', 't'): + repo_tgt = '{0}@{1}'.format(_user, server) + else: + repo_tgt = os.path.abspath(os.path.expanduser(server)) + _cmd.append('{0}:{1}'.format(repo_tgt, + repo['name'])) + 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(repo['name'], + ' '.join(_cmd))) + if not self.args['dryrun']: + _out = subprocess.run(_cmd, + env = _loc_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(repo['name'], _stdout)) + self.logger.debug('[{0}]: STDERR: ({2})\n{1}'.format(repo['name'], + _stderr, + ' '.join( + _cmd))) + if _returncode != 0: + self.logger.error( + '[{0}]: FAILED: {1}'.format(repo['name'], ' '.join(_cmd))) + if _stderr != '' and self.cron and _returncode != 0: + self.logger.warning('Command {0} failed: {1}'.format(' '.join(_cmd), + _stderr)) + del (_loc_env['BORG_PASSPHRASE']) + self.logger.info('[{0}]: END PRUNE'.format(repo['name'])) + self.logger.info('END: prune') + return() def printer(self): # TODO: better alignment. https://stackoverflow.com/a/5676884 @@ -684,6 +758,11 @@ def parseArgs(): parents = [commonargs, remoteargs, fileargs]) + pruneargs = subparsers.add_parser('prune', + help = ('Prune backups to schedule.'), + parents = [commonargs, + remoteargs, + fileargs]) cvrtargs = subparsers.add_parser('convert', help = ('Convert the legacy JSON format to the new XML format and quit')) ### OPERATION-SPECIFIC OPTIONS ### @@ -837,6 +916,8 @@ def main(): bak.createRepo() elif args['oper'] == 'restore': bak.restore() + elif args['oper'] == 'prune': + bak.prune() return() diff --git a/config.xml b/config.xml new file mode 100644 index 0000000..c7023e4 --- /dev/null +++ b/config.xml @@ -0,0 +1,34 @@ + + + + + + + + + + /home/nosbig/git-repos/nosbig/BorgExtend/files + + /home/nosbig/git-repos/nosbig/BorgExtend/files/b.txt + + + + + diff --git a/config.xsd b/config.xsd index 25d0130..c1cdeed 100644 --- a/config.xsd +++ b/config.xsd @@ -118,6 +118,20 @@ + + + + + + + + + + + + + +