Prechádzať zdrojové kódy

In generate-borgmatic-config, comment out all optional config (#57).

Dan Helfman 6 rokov pred
rodič
commit
47efa88c9d

+ 4 - 0
NEWS

@@ -1,3 +1,7 @@
+1.2.5
+ * #57: When generating sample configuration with generate-borgmatic-config, comment out all
+   optional configuration so as to streamline the initial configuration process.
+
 1.2.4
  * Fix for archive checking traceback due to parameter mismatch.
 

+ 1 - 1
README.md

@@ -108,7 +108,7 @@ not in your system `PATH`. Try looking in `/usr/local/bin/`.
 This generates a sample configuration file at /etc/borgmatic/config.yaml (by
 default). You should edit the file to suit your needs, as the values are just
 representative. All fields are optional except where indicated, so feel free
-to remove anything you don't need.
+to ignore anything you don't need.
 
 You can also have a look at the [full configuration
 schema](https://projects.torsion.org/witten/borgmatic/src/master/borgmatic/config/schema.yaml)

+ 57 - 6
borgmatic/config/generate.py

@@ -9,7 +9,7 @@ INDENT = 4
 
 def _insert_newline_before_comment(config, field_name):
     '''
-    Using some ruamel.yaml black magic, insert a blank line in the config right befor the given
+    Using some ruamel.yaml black magic, insert a blank line in the config right before the given
     field and its comments.
     '''
     config.ca.items[field_name][1].insert(
@@ -40,10 +40,58 @@ def _schema_to_sample_configuration(schema, level=0):
     return config
 
 
-def write_configuration(config_filename, config, mode=0o600):
+def _comment_out_line(line):
+    # If it's already is commented out (or empty), there's nothing further to do!
+    stripped_line = line.lstrip()
+    if not stripped_line or stripped_line.startswith('#'):
+        return line
+
+    # Comment out the names of optional sections.
+    one_indent = ' ' * INDENT
+    if not line.startswith(one_indent):
+        return '#' + line
+
+    # Otherwise, comment out the line, but insert the "#" after the first indent for aesthetics.
+    return '#'.join((one_indent, line[INDENT:]))
+
+
+def _comment_out_optional_configuration(rendered_config):
     '''
-    Given a target config filename and a config data structure of nested OrderedDicts, write out the
-    config to file as YAML. Create any containing directories as needed.
+    Post-process a rendered configuration string to comment out optional key/values. The idea is
+    that this prevents the user from having to comment out a bunch of configuration they don't care
+    about to get to a minimal viable configuration file.
+
+    Ideally ruamel.yaml would support this during configuration generation, but it's not terribly
+    easy to accomplish that way.
+    '''
+    lines = []
+    required = False
+
+    for line in rendered_config.split('\n'):
+        # Upon encountering a required configuration option, skip commenting out lines until the
+        # next blank line.
+        stripped_line = line.strip()
+        if stripped_line in {'source_directories:', 'repositories:'} or line == 'location:':
+            required = True
+        elif not stripped_line:
+            required = False
+
+        lines.append(_comment_out_line(line) if not required else line)
+
+    return '\n'.join(lines)
+
+
+def _render_configuration(config):
+    '''
+    Given a config data structure of nested OrderedDicts, render the config as YAML and return it.
+    '''
+    return yaml.round_trip_dump(config, indent=INDENT, block_seq_indent=INDENT)
+
+
+def write_configuration(config_filename, rendered_config, mode=0o600):
+    '''
+    Given a target config filename and rendered config YAML, write it out to file. Create any
+    containing directories as needed.
     '''
     if os.path.exists(config_filename):
         raise FileExistsError('{} already exists. Aborting.'.format(config_filename))
@@ -54,7 +102,7 @@ def write_configuration(config_filename, config, mode=0o600):
         pass
 
     with open(config_filename, 'w') as config_file:
-        config_file.write(yaml.round_trip_dump(config, indent=INDENT, block_seq_indent=INDENT))
+        config_file.write(rendered_config)
 
     os.chmod(config_filename, mode)
 
@@ -90,4 +138,7 @@ def generate_sample_configuration(config_filename, schema_filename):
     schema = yaml.round_trip_load(open(schema_filename))
     config = _schema_to_sample_configuration(schema)
 
-    write_configuration(config_filename, config)
+    write_configuration(
+        config_filename,
+        _comment_out_optional_configuration(_render_configuration(config))
+    )

+ 10 - 10
borgmatic/config/schema.yaml

@@ -18,6 +18,16 @@ map:
                     - /home
                     - /etc
                     - /var/log/syslog*
+            repositories:
+                required: true
+                seq:
+                    - type: scalar
+                desc: |
+                    Paths to local or remote repositories (required). Tildes are expanded. Multiple
+                    repositories are backed up to in sequence. See ssh_command for SSH options like
+                    identity file or port.
+                example:
+                    - user@backupserver:sourcehostname.borg
             one_file_system:
                 type: bool
                 desc: Stay in same file system (do not cross mount points).
@@ -48,16 +58,6 @@ map:
                 type: scalar
                 desc: Alternate Borg remote executable. Defaults to "borg".
                 example: borg1
-            repositories:
-                required: true
-                seq:
-                    - type: scalar
-                desc: |
-                    Paths to local or remote repositories (required). Tildes are expanded. Multiple
-                    repositories are backed up to in sequence. See ssh_command for SSH options like
-                    identity file or port.
-                example:
-                    - user@backupserver:sourcehostname.borg
             patterns:
                 seq:
                     - type: scalar

+ 69 - 4
borgmatic/tests/integration/config/test_generate.py

@@ -16,6 +16,69 @@ def test_insert_newline_before_comment_does_not_raise():
     module._insert_newline_before_comment(config, field_name)
 
 
+def test_comment_out_line_skips_blank_line():
+    line = '    \n'
+
+    assert module._comment_out_line(line) == line
+
+
+def test_comment_out_line_skips_already_commented_out_line():
+    line = '    # foo'
+
+    assert module._comment_out_line(line) == line
+
+
+def test_comment_out_line_comments_section_name():
+    line = 'figgy-pudding:'
+
+    assert module._comment_out_line(line) == '#' + line
+
+
+def test_comment_out_line_comments_indented_option():
+    line = '    enabled: true'
+
+    assert module._comment_out_line(line) == '    #enabled: true'
+
+
+def test_comment_out_optional_configuration_comments_optional_config_only():
+    flexmock(module)._comment_out_line = lambda line: '#' + line
+    config = '''
+foo:
+    bar:
+        - baz
+        - quux
+
+location:
+    repositories:
+        - one
+        - two
+
+    other: thing
+    '''
+
+    expected_config = '''
+#foo:
+#    bar:
+#        - baz
+#        - quux
+#
+location:
+    repositories:
+        - one
+        - two
+#
+#    other: thing
+    '''
+
+    assert module._comment_out_optional_configuration(config.strip()) == expected_config.strip()
+
+
+def test_render_configuration_does_not_raise():
+    flexmock(module.yaml).should_receive('round_trip_dump')
+
+    module._render_configuration({})
+
+
 def test_write_configuration_does_not_raise():
     flexmock(os.path).should_receive('exists').and_return(False)
     flexmock(os).should_receive('makedirs')
@@ -23,14 +86,14 @@ def test_write_configuration_does_not_raise():
     builtins.should_receive('open').and_return(StringIO())
     flexmock(os).should_receive('chmod')
 
-    module.write_configuration('config.yaml', {})
+    module.write_configuration('config.yaml', 'config: yaml')
 
 
 def test_write_configuration_with_already_existing_file_raises():
     flexmock(os.path).should_receive('exists').and_return(True)
 
     with pytest.raises(FileExistsError):
-        module.write_configuration('config.yaml', {})
+        module.write_configuration('config.yaml', 'config: yaml')
 
 
 def test_write_configuration_with_already_existing_directory_does_not_raise():
@@ -40,7 +103,7 @@ def test_write_configuration_with_already_existing_directory_does_not_raise():
     builtins.should_receive('open').and_return(StringIO())
     flexmock(os).should_receive('chmod')
 
-    module.write_configuration('config.yaml', {})
+    module.write_configuration('config.yaml', 'config: yaml')
 
 
 def test_add_comments_to_configuration_does_not_raise():
@@ -59,7 +122,9 @@ def test_add_comments_to_configuration_does_not_raise():
 def test_generate_sample_configuration_does_not_raise():
     builtins = flexmock(sys.modules['builtins'])
     builtins.should_receive('open').with_args('schema.yaml').and_return('')
-    flexmock(module).should_receive('write_configuration')
     flexmock(module).should_receive('_schema_to_sample_configuration')
+    flexmock(module).should_receive('_render_configuration')
+    flexmock(module).should_receive('_comment_out_optional_configuration')
+    flexmock(module).should_receive('write_configuration')
 
     module.generate_sample_configuration('config.yaml', 'schema.yaml')