Browse Source

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

Dan 7 years ago
parent
commit
43d0e597a2

+ 2 - 0
.gitignore

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

+ 4 - 2
NEWS

@@ -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.
- * 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
 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

+ 3 - 1
borgmatic/config/schema.yaml

@@ -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: |

+ 27 - 15
borgmatic/config/validate.py

@@ -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)
 
 
-    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):
     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)

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