|
@@ -1,9 +1,12 @@
|
|
|
+import collections
|
|
|
import io
|
|
|
import os
|
|
|
import re
|
|
|
|
|
|
from ruamel import yaml
|
|
|
|
|
|
+from borgmatic.config import load
|
|
|
+
|
|
|
INDENT = 4
|
|
|
SEQUENCE_INDENT = 2
|
|
|
|
|
@@ -40,8 +43,8 @@ def _schema_to_sample_configuration(schema, level=0, parent_is_sequence=False):
|
|
|
elif 'map' in schema:
|
|
|
config = yaml.comments.CommentedMap(
|
|
|
[
|
|
|
- (section_name, _schema_to_sample_configuration(section_schema, level + 1))
|
|
|
- for section_name, section_schema in schema['map'].items()
|
|
|
+ (field_name, _schema_to_sample_configuration(sub_schema, level + 1))
|
|
|
+ for field_name, sub_schema in schema['map'].items()
|
|
|
]
|
|
|
)
|
|
|
indent = (level * INDENT) + (SEQUENCE_INDENT if parent_is_sequence else 0)
|
|
@@ -68,37 +71,32 @@ def _comment_out_line(line):
|
|
|
return '# '.join((indent_spaces, line[count_indent_spaces:]))
|
|
|
|
|
|
|
|
|
-REQUIRED_KEYS = {'source_directories', 'repositories', 'keep_daily'}
|
|
|
-REQUIRED_SECTION_NAMES = {'location', 'retention'}
|
|
|
-
|
|
|
-
|
|
|
def _comment_out_optional_configuration(rendered_config):
|
|
|
'''
|
|
|
- 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.
|
|
|
+ Post-process a rendered configuration string to comment out optional key/values, as determined
|
|
|
+ by a sentinel in the comment before each key.
|
|
|
|
|
|
- Ideally ruamel.yaml would support this during configuration generation, but it's not terribly
|
|
|
- easy to accomplish that way.
|
|
|
+ The idea is that the pre-commented configuration 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 commenting out keys during configuration generation, but it's
|
|
|
+ not terribly easy to accomplish that way.
|
|
|
'''
|
|
|
lines = []
|
|
|
- required = False
|
|
|
+ optional = False
|
|
|
|
|
|
for line in rendered_config.split('\n'):
|
|
|
- key = line.strip().split(':')[0]
|
|
|
-
|
|
|
- if key in REQUIRED_SECTION_NAMES:
|
|
|
- lines.append(line)
|
|
|
+ # Upon encountering an optional configuration option, commenting out lines until the next
|
|
|
+ # blank line.
|
|
|
+ if line.strip().startswith('# {}'.format(COMMENTED_OUT_SENTINEL)):
|
|
|
+ optional = True
|
|
|
continue
|
|
|
|
|
|
- # Upon encountering a required configuration option, skip commenting out lines until the
|
|
|
- # next blank line.
|
|
|
- if key in REQUIRED_KEYS:
|
|
|
- required = True
|
|
|
- elif not key:
|
|
|
- required = False
|
|
|
+ # Hit a blank line, so reset commenting.
|
|
|
+ if not line.strip():
|
|
|
+ optional = False
|
|
|
|
|
|
- lines.append(_comment_out_line(line) if not required else line)
|
|
|
+ lines.append(_comment_out_line(line) if optional else line)
|
|
|
|
|
|
return '\n'.join(lines)
|
|
|
|
|
@@ -168,6 +166,11 @@ def add_comments_to_configuration_sequence(config, schema, indent=0):
|
|
|
return
|
|
|
|
|
|
|
|
|
+REQUIRED_SECTION_NAMES = {'location', 'retention'}
|
|
|
+REQUIRED_KEYS = {'source_directories', 'repositories', 'keep_daily'}
|
|
|
+COMMENTED_OUT_SENTINEL = 'COMMENT_OUT'
|
|
|
+
|
|
|
+
|
|
|
def add_comments_to_configuration_map(config, schema, indent=0, skip_first=False):
|
|
|
'''
|
|
|
Using descriptions from a schema as a source, add those descriptions as comments to the given
|
|
@@ -178,10 +181,20 @@ def add_comments_to_configuration_map(config, schema, indent=0, skip_first=False
|
|
|
continue
|
|
|
|
|
|
field_schema = schema['map'].get(field_name, {})
|
|
|
- description = field_schema.get('desc')
|
|
|
+ description = field_schema.get('desc', '').strip()
|
|
|
+
|
|
|
+ # If this is an optional key, add an indicator to the comment flagging it to be commented
|
|
|
+ # out from the sample configuration. This sentinel is consumed by downstream processing that
|
|
|
+ # does the actual commenting out.
|
|
|
+ if field_name not in REQUIRED_SECTION_NAMES and field_name not in REQUIRED_KEYS:
|
|
|
+ description = (
|
|
|
+ '\n'.join((description, COMMENTED_OUT_SENTINEL))
|
|
|
+ if description
|
|
|
+ else COMMENTED_OUT_SENTINEL
|
|
|
+ )
|
|
|
|
|
|
# No description to use? Skip it.
|
|
|
- if not field_schema or not description:
|
|
|
+ if not field_schema or not description: # pragma: no cover
|
|
|
continue
|
|
|
|
|
|
config.yaml_set_comment_before_after_key(key=field_name, before=description, indent=indent)
|
|
@@ -190,14 +203,86 @@ def add_comments_to_configuration_map(config, schema, indent=0, skip_first=False
|
|
|
_insert_newline_before_comment(config, field_name)
|
|
|
|
|
|
|
|
|
-def generate_sample_configuration(config_filename, schema_filename):
|
|
|
+RUAMEL_YAML_COMMENTS_INDEX = 1
|
|
|
+
|
|
|
+
|
|
|
+def remove_commented_out_sentinel(config, field_name):
|
|
|
'''
|
|
|
- Given a target config filename and the path to a schema filename in pykwalify YAML schema
|
|
|
- format, write out a sample configuration file based on that schema.
|
|
|
+ Given a configuration CommentedMap and a top-level field name in it, remove any "commented out"
|
|
|
+ sentinel found at the end of its YAML comments. This prevents the given field name from getting
|
|
|
+ commented out by downstream processing that consumes the sentinel.
|
|
|
+ '''
|
|
|
+ try:
|
|
|
+ last_comment_value = config.ca.items[field_name][RUAMEL_YAML_COMMENTS_INDEX][-1].value
|
|
|
+ except KeyError:
|
|
|
+ return
|
|
|
+
|
|
|
+ if last_comment_value == '# {}\n'.format(COMMENTED_OUT_SENTINEL):
|
|
|
+ config.ca.items[field_name][RUAMEL_YAML_COMMENTS_INDEX].pop()
|
|
|
+
|
|
|
+
|
|
|
+def merge_source_configuration_into_destination(destination_config, source_config):
|
|
|
+ '''
|
|
|
+ Deep merge the given source configuration dict into the destination configuration CommentedMap,
|
|
|
+ favoring values from the source when there are collisions.
|
|
|
+
|
|
|
+ The purpose of this is to upgrade configuration files from old versions of borgmatic by adding
|
|
|
+ new
|
|
|
+ configuration keys and comments.
|
|
|
+ '''
|
|
|
+ if not destination_config or not isinstance(source_config, collections.abc.Mapping):
|
|
|
+ return source_config
|
|
|
+
|
|
|
+ for field_name, source_value in source_config.items():
|
|
|
+ # Since this key/value is from the source configuration, leave it uncommented and remove any
|
|
|
+ # sentinel that would cause it to get commented out.
|
|
|
+ remove_commented_out_sentinel(destination_config, field_name)
|
|
|
+
|
|
|
+ # This is a mapping. Recurse for this key/value.
|
|
|
+ if isinstance(source_value, collections.abc.Mapping):
|
|
|
+ destination_config[field_name] = merge_source_configuration_into_destination(
|
|
|
+ destination_config[field_name], source_value
|
|
|
+ )
|
|
|
+ continue
|
|
|
+
|
|
|
+ # This is a sequence. Recurse for each item in it.
|
|
|
+ if isinstance(source_value, collections.abc.Sequence) and not isinstance(source_value, str):
|
|
|
+ destination_value = destination_config[field_name]
|
|
|
+ destination_config[field_name] = yaml.comments.CommentedSeq(
|
|
|
+ [
|
|
|
+ merge_source_configuration_into_destination(
|
|
|
+ destination_value[index] if index < len(destination_value) else None,
|
|
|
+ source_item,
|
|
|
+ )
|
|
|
+ for index, source_item in enumerate(source_value)
|
|
|
+ ]
|
|
|
+ )
|
|
|
+ continue
|
|
|
+
|
|
|
+ # This is some sort of scalar. Simply set it into the destination.
|
|
|
+ destination_config[field_name] = source_config[field_name]
|
|
|
+
|
|
|
+ return destination_config
|
|
|
+
|
|
|
+
|
|
|
+def generate_sample_configuration(source_filename, destination_filename, schema_filename):
|
|
|
+ '''
|
|
|
+ Given an optional source configuration filename, and a required destination configuration
|
|
|
+ filename, and the path to a schema filename in pykwalify YAML schema format, write out a
|
|
|
+ sample configuration file based on that schema. If a source filename is provided, merge the
|
|
|
+ parsed contents of that configuration into the generated configuration.
|
|
|
'''
|
|
|
schema = yaml.round_trip_load(open(schema_filename))
|
|
|
- config = _schema_to_sample_configuration(schema)
|
|
|
+ source_config = None
|
|
|
+
|
|
|
+ if source_filename:
|
|
|
+ source_config = load.load_configuration(source_filename)
|
|
|
+
|
|
|
+ destination_config = merge_source_configuration_into_destination(
|
|
|
+ _schema_to_sample_configuration(schema), source_config
|
|
|
+ )
|
|
|
|
|
|
write_configuration(
|
|
|
- config_filename, _comment_out_optional_configuration(_render_configuration(config))
|
|
|
+ destination_filename,
|
|
|
+ _comment_out_optional_configuration(_render_configuration(destination_config)),
|
|
|
)
|