Compare commits
No commits in common. "master" and "pruning" have entirely different histories.
56
backup.py
56
backup.py
@ -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:
|
||||
|
23
config.xsd
23
config.xsd
@ -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 -->
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user