Compare commits

..

2 Commits

Author SHA1 Message Date
407eeb2d1b
minor terminology change 2022-05-23 03:01:13 -04:00
8e7841b4cc
fix. --keep-within and --keep-<period> are different options. 2022-05-22 20:18:05 -04:00
4 changed files with 92 additions and 45 deletions

View File

@ -45,6 +45,16 @@ 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):
@ -127,9 +137,6 @@ 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
@ -164,11 +171,20 @@ 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
retention = repo.attrib.get('pruneRetention') keepWithin = repo.find('{0}keepWithin'.format(self.ns))
if retention is not None: if keepWithin is not None:
r['retention'] = isodate.parse_duration(retention) if 'retention' not in r.keys():
else: r['retention'] = {}
r['retention'] = dfltRetention 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)
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)))
@ -482,12 +498,8 @@ 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 duration was set. Skip. # No prune retention 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']))
@ -500,8 +512,18 @@ class Backup(object):
'--log-json', '--log-json',
'--{0}'.format(self.args['loglevel']), '--{0}'.format(self.args['loglevel']),
'prune', 'prune',
'--stats', '--stats']
'--keep-secondly', int(retentionSeconds)] # 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]])
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:

View File

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



<!-- START ROOT --> <!-- START ROOT -->
<xs:element name="borg"> <xs:element name="borg">
<xs:complexType> <xs:complexType>
@ -56,6 +57,24 @@
<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"/>
@ -100,8 +119,6 @@
<!-- 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"/>
@ -124,8 +141,6 @@
<!-- 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 -->

View File

@ -1,8 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<repo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <repo name="testrepo2"
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>

View File

@ -12,17 +12,9 @@
"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.
--> -->
<!--
The pruneRetention attribute's format (ISO 8601 Duration) can be found here: <server target="fq.dn.tld" remote="true" rsh="ssh -p 22" user="root">
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.
@ -30,20 +22,41 @@
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. --> <!--
<!-- Each path entry is a path to back up. 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 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 Note that globbing, etc. is *disabled* for security reasons, so you will need to specify all
directories explicitly. --> 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, obviously). --> <!-- Each exclude entry should be a subdirectory of a <path> (otherwise it wouldn't match). -->
<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