Compare commits

..

No commits in common. "master" and "pruning" have entirely different histories.

4 changed files with 45 additions and 92 deletions

View File

@ -45,16 +45,6 @@ loglvls = {'critical': logging.CRITICAL,
### DEFAULT NAMESPACE ###
dflt_ns = 'http://git.root2.io/r00t2/borgextend/'

# In code, replace "day" with "daily" when constructing command.
policyperiods = (
('seconds', 'secondly'),
('minutes', 'minutely'),
('hours', 'hourly'),
('days', 'daily'),
('weeks', 'weekly'),
('months', 'monthly'),
('years', 'yearly'),
)

### THE GUTS ###
class Backup(object):
@ -137,6 +127,9 @@ class Backup(object):
if not reponames:
reponames = []
repos = []
dfltRetention = None
if server.attrib.get('pruneRetention') is not None:
dfltRetention = isodate.parse_duration(server.attrib.get('pruneRetention'))
for repo in server.findall('{0}repo'.format(self.ns)):
if reponames and repo.attrib['name'] not in reponames:
continue
@ -147,8 +140,8 @@ class Backup(object):
r[a] = repo.attrib[a]
for e in ('path', 'exclude'):
# TODO: have an attrib for path and exclude, "glob=<bool>"?
# If true, try using the glob module to resolve paths?
# This gives us the benefit of allowing glob per-path/exclude.
# If true, try using the glob module to resolve paths?
# This gives us the benefit of allowing glob per-path/exclude.
r[e] = [i.text for i in repo.findall(self.ns + e)]
for prep in repo.findall('{0}prep'.format(self.ns)):
if 'prep' not in r:
@ -171,20 +164,11 @@ class Backup(object):
r['plugins'][pname]['params'][paramname] = json.loads(param.text)
else:
r['plugins'][pname]['params'][paramname] = param.text
keepWithin = repo.find('{0}keepWithin'.format(self.ns))
if keepWithin is not None:
if 'retention' not in r.keys():
r['retention'] = {}
r['retention']['last'] = isodate.parse_duration(keepWithin.text)
keepLast = repo.find('{0}keepLast'.format(self.ns))
if keepLast is not None:
for e, _ in policyperiods:
policy = keepLast.find('{0}{1}'.format(self.ns, e))
if policy is not None:
if 'retention' not in r.keys():
r['retention'] = {}
# This is safe. We validate the config.
r['retention'][e] = int(policy.text)
retention = repo.attrib.get('pruneRetention')
if retention is not None:
r['retention'] = isodate.parse_duration(retention)
else:
r['retention'] = dfltRetention
repos.append(r)
return(repos)
self.logger.debug('VARS (before args cleanup): {0}'.format(vars(self)))
@ -498,8 +482,12 @@ class Backup(object):
_user = self.repos[server].get('user', pwd.getpwuid(os.geteuid()).pw_name)
for repo in self.repos[server]['repos']:
if repo.get('retention') is None:
# No prune retention was set. Skip.
# No prune duration was set. Skip.
continue
if isinstance(repo['retention'], datetime.timedelta):
retentionSeconds = repo['retention'].total_seconds()
else: # it's an isodate.Duration
retentionSeconds = repo['retention'].totimedelta(datetime.datetime.now()).total_seconds()
_loc_env = _env.copy()
if 'password' not in repo:
print('Password not supplied for {0}:{1}.'.format(server, repo['name']))
@ -512,18 +500,8 @@ class Backup(object):
'--log-json',
'--{0}'.format(self.args['loglevel']),
'prune',
'--stats']
# keepWithin
if repo['retention'].get('last') is not None:
if isinstance(repo['retention'].get('last'), datetime.timedelta): # it's a time.timedelta
retentionSeconds = repo['retention'].total_seconds()
else: # it's an isodate.Duration
retentionSeconds = repo['retention'].totimedelta(datetime.datetime.now()).total_seconds()
_cmd.extend(['--keep-within', '{0}H'.format(retentionSeconds / 60)])
# keepLast
for e, a in policyperiods:
if repo['retention'].get(e, 0) is not 0:
_cmd.extend(['--keep-{0}'.format(a), repo['retention'][e]])
'--stats',
'--keep-secondly', int(retentionSeconds)]
if self.repos[server]['remote'].lower()[0] in ('1', 't'):
repo_tgt = '{0}@{1}'.format(_user, server)
else:

View File

@ -43,7 +43,6 @@
</xs:simpleContent>
</xs:complexType>


<!-- START ROOT -->
<xs:element name="borg">
<xs:complexType>
@ -57,24 +56,6 @@
<xs:element name="repo" minOccurs="1" maxOccurs="unbounded">
<xs:complexType>
<xs:choice minOccurs="1" maxOccurs="unbounded">
<!-- START RETENTION POLICY -->
<!-- This is used to specify the per-repo retention *period* for pruning. -->
<xs:element name="keepWithin" minOccurs="0" maxOccurs="1" type="xs:duration"/>
<!-- This is used to specify more advanced retentions. -->
<xs:element name="keepLast" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:choice minOccurs="1" maxOccurs="7">
<xs:element name="seconds" minOccurs="0" maxOccurs="1" type="xs:positiveInteger"/>
<xs:element name="minutes" minOccurs="0" maxOccurs="1" type="xs:positiveInteger"/>
<xs:element name="hours" minOccurs="0" maxOccurs="1" type="xs:positiveInteger"/>
<xs:element name="days" minOccurs="0" maxOccurs="1" type="xs:positiveInteger"/>
<xs:element name="weeks" minOccurs="0" maxOccurs="1" type="xs:positiveInteger"/>
<xs:element name="months" minOccurs="0" maxOccurs="1" type="xs:positiveInteger"/>
<xs:element name="years" minOccurs="0" maxOccurs="1" type="xs:positiveInteger"/>
</xs:choice>
</xs:complexType>
</xs:element>
<!-- END RETENTION POLICY -->
<!-- START PATH -->
<xs:element name="path" minOccurs="1"
maxOccurs="unbounded" type="xs:anyURI"/>
@ -119,6 +100,8 @@
<!-- This specifies if a repo is a "dummy" configuration.
Useful for testing and placeholder. -->
<xs:attribute name="dummy" type="xs:boolean" use="optional" default="false"/>
<!-- This is used to specify the per-repo retention period for pruning. -->
<xs:attribute name="pruneRetention" type="xs:duration" use="optional"/>
</xs:complexType>
<xs:unique name="uniquePath">
<xs:selector xpath="path"/>
@ -141,6 +124,8 @@
<!-- 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"/>
<!-- This is used to specify the server-wide default retention period for pruning. -->
<xs:attribute name="pruneRetention" type="xs:duration" use="optional"/>
</xs:complexType>
</xs:element>
<!-- END SERVER -->

View File

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<repo name="testrepo2"
<repo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://git.root2.io/r00t2/borgextend/"
xsi:schemaLocation="http://git.root2.io/r00t2/borgextend/ http://git.r00t2.io/r00t2/borgextend/src/branch/master/config.xsd"
name="testrepo2"
password="AnotherSuperSecretPassword"
dummy="true">
<path>/dev/null</path>

View File

@ -12,9 +12,17 @@
"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.
"pruneRetention" = an ISO 8601 duration to be used as the default rentention period during a prune operation.
-->

<server target="fq.dn.tld" remote="true" rsh="ssh -p 22" user="root">
<!--
The pruneRetention attribute's format (ISO 8601 Duration) can be found here:
https://en.wikipedia.org/wiki/ISO_8601#Durations).
It must be positive. The "alternative"/"extended" variant (the one using colons) should be supported
(but may be unpredictable).
It is optional, but can be used as a fallback during a prune operation if a repo child does not specify one.
The below example would prune anything older than 1 month, 2 minutes, and 30 seconds.
-->
<server target="fq.dn.tld" remote="true" rsh="ssh -p 22" user="root" pruneRetention="P1MT2M30S">
<!-- You can (and probably will) have multiple repos for each server. -->
<!--
"name" = the repository name.
@ -22,41 +30,20 @@
to enter it interactively and securely.
"dummy" = see server[@dummy] explanation.
"compression" = see https://borgbackup.readthedocs.io/en/stable/usage/create.html (-C option)
"pruneRetention" = an ISO 8601 duration to be used as the rentention period during a prune operation.
If not specified, uses ../server/[@pruneRetention].
If ../server/[@pruneRetention] was not specified, this repo will be skipped during a prune.
-->
<repo name="testrepo"
password="SuperSecretPassword"
compression="lzma,9">
<!--
keepWithin specifies a flat time period for archive retention during a prune.
The keepWithin attribute's format (ISO 8601 Duration) can be found here:
https://en.wikipedia.org/wiki/ISO_8601#Durations).
It must be positive. The "alternative"/"extended" variant (the one using colons) should be supported
(but may be unpredictable).
It is optional. If no keepWithin or keepLast (below) is specified, this repo will be skipped during
prune operations.
The below example would prune anything older than 1 month, 2 minutes, and 30 seconds.
If both keepWithin and keepLast is provided, keepLast only affects archives *before* the time period
specified by keepWithin.
-->
<keepWithin>P1MT2M30S</keepWithin>
<!--
keepLast specifies a *policy* for retention.
It can be used to specify explicit "N archives per Y period type". For example, the below retains the last
archive for each month (going back 2 months), and the last archive each year for the last three years.
-->
<keepLast>
<months>2</months>
<years>3</years>
</keepLast>
<!--
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
parent directories explicitly.
Recursing is enabled, though.
-->
compression="lzma,9"
pruneRetention="P1Y"><!-- One year. -->
<!-- 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>/a</path>
<!-- Each exclude entry should be a subdirectory of a <path> (otherwise it wouldn't match). -->
<!-- Each exclude entry should be a subdirectory of a <path> (otherwise it wouldn't match, obviously). -->
<exclude>/a/b</exclude>
<!-- Prep items are executed in non-guaranteed order (but are likely to be performed in order given).
If you require them to be in a specific order, you should use a wrapper script and