Added XSD Support for Pruning Old Backups
This commit is contained in:
parent
7ef21e8059
commit
9890b671f2
9
.gitignore
vendored
9
.gitignore
vendored
@ -25,3 +25,12 @@ __pycache__/
|
||||
*.sqlite3
|
||||
*.deb
|
||||
.idea/
|
||||
files/*
|
||||
repo/*
|
||||
repo\:testrepo/*
|
||||
config
|
||||
hints.21
|
||||
index.21
|
||||
integrity.21
|
||||
nonce
|
||||
README
|
81
backup.py
81
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()
|
||||
|
||||
|
||||
|
34
config.xml
Normal file
34
config.xml
Normal file
@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<borg xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://git.square-r00t.net/BorgExtend/tree/storage/backups/borg/"
|
||||
xsi:schemaLocation="http://git.square-r00t.net/BorgExtend/plain/config.xsd"
|
||||
xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||
<!-- You can have multiple server elements, but each one *MUST* have a unique "target" attribute. -->
|
||||
<!-- "target" = either the local filesystem path (absolute or relative to execution) or the remote host
|
||||
"remote" = 1/true if "target" is a remote host or 0/false if it's a local filepath
|
||||
"rsh" = (remote host only) the ssh command to use. The default is given below.
|
||||
"user" = (remote host only) the ssh user to use.
|
||||
"dummy" = a boolean; if you need to create a "dummy" server, set this to "true".
|
||||
It will *not* be parsed or executed upon.
|
||||
It won't even be created by an init operation or show up in a repolist operation. -->
|
||||
<server target="repo" remote="false" keepSecondly="3" keepMinutely="2">
|
||||
<!-- You can (and probably will) have multiple repos for each server. -->
|
||||
<!-- "name" = the repositoriy name.
|
||||
"password" = the repository's password for the key. If not specified, you will be prompted
|
||||
to enter it interactively and securely.
|
||||
"dummy" = see server[@dummy] explanation.
|
||||
"compression" = see https://borgbackup.readthedocs.io/en/stable/usage/create.html (-C option) -->
|
||||
<repo name="testrepo" password="SuperSecretPassword" compression="lzma,9">
|
||||
<!-- Each path entry is a path to back up.
|
||||
See https://borgbackup.readthedocs.io/en/stable/usage/create.html
|
||||
Note that globbing, etc. is *disabled* for security reasons, so you will need to specify all
|
||||
directories explicitly. -->
|
||||
<path>/home/nosbig/git-repos/nosbig/BorgExtend/files</path>
|
||||
<!-- Each exclude entry should be a subdirectory of a <path> (otherwise it wouldn't match, obviously). -->
|
||||
<exclude>/home/nosbig/git-repos/nosbig/BorgExtend/files/b.txt</exclude>
|
||||
</repo>
|
||||
<!-- You can also include other snippets. Either absolute paths, paths relative to your backup.xml file,
|
||||
or a URL. -->
|
||||
<xi:include href="sample.config.snippet.xml"/>
|
||||
</server>
|
||||
</borg>
|
14
config.xsd
14
config.xsd
@ -118,6 +118,20 @@
|
||||
<!-- This specifies if a server is a "dummy" configuration.
|
||||
Useful for testing and placeholder. -->
|
||||
<xs:attribute name="dummy" type="xs:boolean" use="optional" default="false"/>
|
||||
<!-- Used to define the number of yearly backups to keep. -->
|
||||
<xs:attribute name="keepYearly" type="xs:int" use="optional" />
|
||||
<!-- Used to define the number of monthly backups to keep. -->
|
||||
<xs:attribute name="keepMonthly" type="xs:int" use="optional" />
|
||||
<!-- Used to define the number of weekly backups to keep. -->
|
||||
<xs:attribute name="keepWeekly" type="xs:int" use="optional" />
|
||||
<!-- Used to define the number of daily backups to keep. -->
|
||||
<xs:attribute name="keepDaily" type="xs:int" use="optional" />
|
||||
<!-- Used to define the number of hourly backups to keep. -->
|
||||
<xs:attribute name="keepHourly" type="xs:int" use="optional" />
|
||||
<!-- Used to define the number of minutely backups to keep. -->
|
||||
<xs:attribute name="keepMinutely" type="xs:int" use="optional" />
|
||||
<!-- Used to define the number of secondly backups to keep. -->
|
||||
<xs:attribute name="keepSecondly" type="xs:int" use="optional" />
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<!-- END SERVER -->
|
||||
|
Loading…
Reference in New Issue
Block a user