Require "prefix" in retention section when "archive_name_format" is set.

This commit is contained in:
Dan 2017-10-29 19:36:26 -07:00
parent f1c07b5cf5
commit 43d0e597a2
6 changed files with 79 additions and 25 deletions

2
.gitignore vendored
View File

@ -1,6 +1,8 @@
*.egg-info *.egg-info
*.pyc *.pyc
*.swp *.swp
.cache
.coverage
.tox .tox
build build
dist dist

6
NEWS
View File

@ -2,8 +2,10 @@
* #16, #38: Support for user-defined hooks before/after backup, or on error. * #16, #38: Support for user-defined hooks before/after backup, or on error.
* #33: Improve clarity of logging spew at high verbosity levels. * #33: Improve clarity of logging spew at high verbosity levels.
* #29: Support for using tilde in source directory path to reference home directory. * #29: Support for using tilde in source directory path to reference home directory.
* Converted main source repository from Mercurial to Git. * Require "prefix" in retention section when "archive_name_format" is set. This is to avoid
* Updated dead links to Borg documentation. accidental pruning of archives with a different archive name format.
* Convert main source repository from Mercurial to Git.
* Update dead links to Borg documentation.
1.1.8 1.1.8
* #39: Fix to make /etc/borgmatic/config.yaml optional rather than required when using the default * #39: Fix to make /etc/borgmatic/config.yaml optional rather than required when using the default

View File

@ -94,7 +94,9 @@ map:
desc: | desc: |
Name of the archive. Borg placeholders can be used. See the output of Name of the archive. Borg placeholders can be used. See the output of
"borg help placeholders" for details. Default is "borg help placeholders" for details. Default is
"{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}" "{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}". If you specify this option, you must
also specify a prefix in the retention section to avoid accidental pruning of
archives with a different archive name format.
example: "{hostname}-documents-{now}" example: "{hostname}-documents-{now}"
retention: retention:
desc: | desc: |

View File

@ -25,6 +25,31 @@ class Validation_error(ValueError):
self.config_filename = config_filename self.config_filename = config_filename
self.error_messages = error_messages self.error_messages = error_messages
def __str__(self):
'''
Render a validation error as a user-facing string.
'''
return 'An error occurred while parsing a configuration file at {}:\n'.format(
self.config_filename
) + '\n'.join(self.error_messages)
def apply_logical_validation(config_filename, parsed_configuration):
'''
Given a parsed and schematically valid configuration as a data structure of nested dicts (see
below), run through any additional logical validation checks. If there are any such validation
problems, raise a Validation_error.
'''
archive_name_format = parsed_configuration.get('storage', {}).get('archive_name_format')
prefix = parsed_configuration.get('retention', {}).get('prefix')
if archive_name_format and not prefix:
raise Validation_error(
config_filename, (
'If you provide an archive_name_format, you must also specify a retention prefix.',
)
)
def parse_configuration(config_filename, schema_filename): def parse_configuration(config_filename, schema_filename):
''' '''
@ -58,19 +83,6 @@ def parse_configuration(config_filename, schema_filename):
if validator.validation_errors: if validator.validation_errors:
raise Validation_error(config_filename, validator.validation_errors) raise Validation_error(config_filename, validator.validation_errors)
apply_logical_validation(config_filename, parsed_result)
return parsed_result return parsed_result
def display_validation_error(validation_error):
'''
Given a Validation_error, display its error messages to stderr.
'''
print(
'An error occurred while parsing a configuration file at {}:'.format(
validation_error.config_filename
),
file=sys.stderr,
)
for error in validation_error.error_messages:
print(error, file=sys.stderr)

View File

@ -148,10 +148,3 @@ def test_parse_configuration_raises_for_validation_error():
with pytest.raises(module.Validation_error): with pytest.raises(module.Validation_error):
module.parse_configuration('config.yaml', 'schema.yaml') module.parse_configuration('config.yaml', 'schema.yaml')
def test_display_validation_error_does_not_raise():
flexmock(sys.modules['builtins']).should_receive('print')
error = module.Validation_error('config.yaml', ('oops', 'uh oh'))
module.display_validation_error(error)

View File

@ -0,0 +1,43 @@
import pytest
from borgmatic.config import validate as module
def test_validation_error_str_contains_error_messages_and_config_filename():
error = module.Validation_error('config.yaml', ('oops', 'uh oh'))
result = str(error)
assert 'config.yaml' in result
assert 'oops' in result
assert 'uh oh' in result
def test_apply_logical_validation_raises_if_archive_name_format_present_without_prefix():
with pytest.raises(module.Validation_error):
module.apply_logical_validation(
'config.yaml',
{
'storage': {'archive_name_format': '{hostname}-{now}'},
'retention': {'keep_daily': 7},
},
)
def test_apply_logical_validation_does_not_raise_if_archive_name_format_and_prefix_present():
module.apply_logical_validation(
'config.yaml',
{
'storage': {'archive_name_format': '{hostname}-{now}'},
'retention': {'prefix': '{hostname}-'},
},
)
def test_apply_logical_validation_does_not_raise_otherwise():
module.apply_logical_validation(
'config.yaml',
{
'retention': {'keep_secondly': 1000},
},
)