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 ###
|
### DEFAULT NAMESPACE ###
|
||||||
dflt_ns = 'http://git.root2.io/r00t2/borgextend/'
|
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 ###
|
### THE GUTS ###
|
||||||
class Backup(object):
|
class Backup(object):
|
||||||
@ -137,6 +127,9 @@ class Backup(object):
|
|||||||
if not reponames:
|
if not reponames:
|
||||||
reponames = []
|
reponames = []
|
||||||
repos = []
|
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)):
|
for repo in server.findall('{0}repo'.format(self.ns)):
|
||||||
if reponames and repo.attrib['name'] not in reponames:
|
if reponames and repo.attrib['name'] not in reponames:
|
||||||
continue
|
continue
|
||||||
@ -147,8 +140,8 @@ class Backup(object):
|
|||||||
r[a] = repo.attrib[a]
|
r[a] = repo.attrib[a]
|
||||||
for e in ('path', 'exclude'):
|
for e in ('path', 'exclude'):
|
||||||
# TODO: have an attrib for path and exclude, "glob=<bool>"?
|
# TODO: have an attrib for path and exclude, "glob=<bool>"?
|
||||||
# If true, try using the glob module to resolve paths?
|
# If true, try using the glob module to resolve paths?
|
||||||
# This gives us the benefit of allowing glob per-path/exclude.
|
# This gives us the benefit of allowing glob per-path/exclude.
|
||||||
r[e] = [i.text for i in repo.findall(self.ns + e)]
|
r[e] = [i.text for i in repo.findall(self.ns + e)]
|
||||||
for prep in repo.findall('{0}prep'.format(self.ns)):
|
for prep in repo.findall('{0}prep'.format(self.ns)):
|
||||||
if 'prep' not in r:
|
if 'prep' not in r:
|
||||||
@ -171,20 +164,11 @@ class Backup(object):
|
|||||||
r['plugins'][pname]['params'][paramname] = json.loads(param.text)
|
r['plugins'][pname]['params'][paramname] = json.loads(param.text)
|
||||||
else:
|
else:
|
||||||
r['plugins'][pname]['params'][paramname] = param.text
|
r['plugins'][pname]['params'][paramname] = param.text
|
||||||
keepWithin = repo.find('{0}keepWithin'.format(self.ns))
|
retention = repo.attrib.get('pruneRetention')
|
||||||
if keepWithin is not None:
|
if retention is not None:
|
||||||
if 'retention' not in r.keys():
|
r['retention'] = isodate.parse_duration(retention)
|
||||||
r['retention'] = {}
|
else:
|
||||||
r['retention']['last'] = isodate.parse_duration(keepWithin.text)
|
r['retention'] = dfltRetention
|
||||||
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)
|
|
||||||
repos.append(r)
|
repos.append(r)
|
||||||
return(repos)
|
return(repos)
|
||||||
self.logger.debug('VARS (before args cleanup): {0}'.format(vars(self)))
|
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)
|
_user = self.repos[server].get('user', pwd.getpwuid(os.geteuid()).pw_name)
|
||||||
for repo in self.repos[server]['repos']:
|
for repo in self.repos[server]['repos']:
|
||||||
if repo.get('retention') is None:
|
if repo.get('retention') is None:
|
||||||
# No prune retention was set. Skip.
|
# No prune duration was set. Skip.
|
||||||
continue
|
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()
|
_loc_env = _env.copy()
|
||||||
if 'password' not in repo:
|
if 'password' not in repo:
|
||||||
print('Password not supplied for {0}:{1}.'.format(server, repo['name']))
|
print('Password not supplied for {0}:{1}.'.format(server, repo['name']))
|
||||||
@ -512,18 +500,8 @@ class Backup(object):
|
|||||||
'--log-json',
|
'--log-json',
|
||||||
'--{0}'.format(self.args['loglevel']),
|
'--{0}'.format(self.args['loglevel']),
|
||||||
'prune',
|
'prune',
|
||||||
'--stats']
|
'--stats',
|
||||||
# keepWithin
|
'--keep-secondly', int(retentionSeconds)]
|
||||||
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]])
|
|
||||||
if self.repos[server]['remote'].lower()[0] in ('1', 't'):
|
if self.repos[server]['remote'].lower()[0] in ('1', 't'):
|
||||||
repo_tgt = '{0}@{1}'.format(_user, server)
|
repo_tgt = '{0}@{1}'.format(_user, server)
|
||||||
else:
|
else:
|
||||||
|
23
config.xsd
23
config.xsd
@ -43,7 +43,6 @@
|
|||||||
</xs:simpleContent>
|
</xs:simpleContent>
|
||||||
</xs:complexType>
|
</xs:complexType>
|
||||||
|
|
||||||
|
|
||||||
<!-- START ROOT -->
|
<!-- START ROOT -->
|
||||||
<xs:element name="borg">
|
<xs:element name="borg">
|
||||||
<xs:complexType>
|
<xs:complexType>
|
||||||
@ -57,24 +56,6 @@
|
|||||||
<xs:element name="repo" minOccurs="1" maxOccurs="unbounded">
|
<xs:element name="repo" minOccurs="1" maxOccurs="unbounded">
|
||||||
<xs:complexType>
|
<xs:complexType>
|
||||||
<xs:choice minOccurs="1" maxOccurs="unbounded">
|
<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 -->
|
<!-- START PATH -->
|
||||||
<xs:element name="path" minOccurs="1"
|
<xs:element name="path" minOccurs="1"
|
||||||
maxOccurs="unbounded" type="xs:anyURI"/>
|
maxOccurs="unbounded" type="xs:anyURI"/>
|
||||||
@ -119,6 +100,8 @@
|
|||||||
<!-- This specifies if a repo is a "dummy" configuration.
|
<!-- This specifies if a repo is a "dummy" configuration.
|
||||||
Useful for testing and placeholder. -->
|
Useful for testing and placeholder. -->
|
||||||
<xs:attribute name="dummy" type="xs:boolean" use="optional" default="false"/>
|
<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:complexType>
|
||||||
<xs:unique name="uniquePath">
|
<xs:unique name="uniquePath">
|
||||||
<xs:selector xpath="path"/>
|
<xs:selector xpath="path"/>
|
||||||
@ -141,6 +124,8 @@
|
|||||||
<!-- This specifies if a server is a "dummy" configuration.
|
<!-- This specifies if a server is a "dummy" configuration.
|
||||||
Useful for testing and placeholder. -->
|
Useful for testing and placeholder. -->
|
||||||
<xs:attribute name="dummy" type="xs:boolean" use="optional" default="false"/>
|
<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:complexType>
|
||||||
</xs:element>
|
</xs:element>
|
||||||
<!-- END SERVER -->
|
<!-- END SERVER -->
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-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"
|
password="AnotherSuperSecretPassword"
|
||||||
dummy="true">
|
dummy="true">
|
||||||
<path>/dev/null</path>
|
<path>/dev/null</path>
|
||||||
|
@ -12,9 +12,17 @@
|
|||||||
"dummy" = a boolean; if you need to create a "dummy" server, set this to "true".
|
"dummy" = a boolean; if you need to create a "dummy" server, set this to "true".
|
||||||
It will *not* be parsed or executed upon.
|
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.
|
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. -->
|
<!-- You can (and probably will) have multiple repos for each server. -->
|
||||||
<!--
|
<!--
|
||||||
"name" = the repository name.
|
"name" = the repository name.
|
||||||
@ -22,41 +30,20 @@
|
|||||||
to enter it interactively and securely.
|
to enter it interactively and securely.
|
||||||
"dummy" = see server[@dummy] explanation.
|
"dummy" = see server[@dummy] explanation.
|
||||||
"compression" = see https://borgbackup.readthedocs.io/en/stable/usage/create.html (-C option)
|
"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"
|
<repo name="testrepo"
|
||||||
password="SuperSecretPassword"
|
password="SuperSecretPassword"
|
||||||
compression="lzma,9">
|
compression="lzma,9"
|
||||||
<!--
|
pruneRetention="P1Y"><!-- One year. -->
|
||||||
keepWithin specifies a flat time period for archive retention during a prune.
|
<!-- Each path entry is a path to back up.
|
||||||
The keepWithin attribute's format (ISO 8601 Duration) can be found here:
|
See https://borgbackup.readthedocs.io/en/stable/usage/create.html
|
||||||
https://en.wikipedia.org/wiki/ISO_8601#Durations).
|
Note that globbing, etc. is *disabled* for security reasons, so you will need to specify all
|
||||||
It must be positive. The "alternative"/"extended" variant (the one using colons) should be supported
|
directories explicitly. -->
|
||||||
(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.
|
|
||||||
-->
|
|
||||||
<path>/a</path>
|
<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>
|
<exclude>/a/b</exclude>
|
||||||
<!-- Prep items are executed in non-guaranteed order (but are likely to be performed in order given).
|
<!-- 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
|
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