Просмотр исходного кода

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

Dan 7 лет назад
Родитель
Сommit
43d0e597a2

+ 2 - 0
.gitignore

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

+ 4 - 2
NEWS

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

+ 3 - 1
borgmatic/config/schema.yaml

@@ -94,7 +94,9 @@ map:
                 desc: |
                     Name of the archive. Borg placeholders can be used. See the output of
                     "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}"
     retention:
         desc: |

+ 27 - 15
borgmatic/config/validate.py

@@ -25,6 +25,31 @@ class Validation_error(ValueError):
         self.config_filename = config_filename
         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):
     '''
@@ -58,19 +83,6 @@ def parse_configuration(config_filename, schema_filename):
     if validator.validation_errors:
         raise Validation_error(config_filename, validator.validation_errors)
 
-    return parsed_result
-
+    apply_logical_validation(config_filename, 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)
+    return parsed_result

+ 0 - 7
borgmatic/tests/integration/config/test_validate.py

@@ -148,10 +148,3 @@ def test_parse_configuration_raises_for_validation_error():
 
     with pytest.raises(module.Validation_error):
         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)

+ 43 - 0
borgmatic/tests/unit/config/test_validate.py

@@ -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},
+        },
+    )