From 47efa88c9dff15fd5d924faff870180d559f4bc1 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Sat, 29 Sep 2018 15:03:11 -0700 Subject: [PATCH] In generate-borgmatic-config, comment out all optional config (#57). --- NEWS | 4 + README.md | 2 +- borgmatic/config/generate.py | 63 ++++++++++++++-- borgmatic/config/schema.yaml | 20 ++--- .../tests/integration/config/test_generate.py | 73 ++++++++++++++++++- 5 files changed, 141 insertions(+), 21 deletions(-) diff --git a/NEWS b/NEWS index 0aabf6d19..3a6aa6271 100644 --- a/NEWS +++ b/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. diff --git a/README.md b/README.md index 849460d95..1c882b4a0 100644 --- a/README.md +++ b/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) diff --git a/borgmatic/config/generate.py b/borgmatic/config/generate.py index 2bfc3a259..6cff0bc4f 100644 --- a/borgmatic/config/generate.py +++ b/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)) + ) diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index 866dc8604..5f39ba258 100644 --- a/borgmatic/config/schema.yaml +++ b/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 diff --git a/borgmatic/tests/integration/config/test_generate.py b/borgmatic/tests/integration/config/test_generate.py index 048315a22..a6fbeeaac 100644 --- a/borgmatic/tests/integration/config/test_generate.py +++ b/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')