Upgrade your borgmatic configuration to get new options and comments via "generate-borgmatic-config --source" (#239).
This commit is contained in:
parent
08f017bc3e
commit
2115eeb6a2
9 changed files with 305 additions and 40 deletions
5
NEWS
5
NEWS
|
@ -1,3 +1,8 @@
|
||||||
|
1.4.7
|
||||||
|
* #239: Upgrade your borgmatic configuration to get new options and comments via
|
||||||
|
"generate-borgmatic-config --source". See the documentation for more information:
|
||||||
|
https://torsion.org/borgmatic/docs/how-to/upgrade/#upgrading-your-configuration
|
||||||
|
|
||||||
1.4.6
|
1.4.6
|
||||||
* Verbosity level "-1" for even quieter output: Errors only (#236).
|
* Verbosity level "-1" for even quieter output: Errors only (#236).
|
||||||
|
|
||||||
|
|
|
@ -12,12 +12,18 @@ def parse_arguments(*arguments):
|
||||||
them as an ArgumentParser instance.
|
them as an ArgumentParser instance.
|
||||||
'''
|
'''
|
||||||
parser = ArgumentParser(description='Generate a sample borgmatic YAML configuration file.')
|
parser = ArgumentParser(description='Generate a sample borgmatic YAML configuration file.')
|
||||||
|
parser.add_argument(
|
||||||
|
'-s',
|
||||||
|
'--source',
|
||||||
|
dest='source_filename',
|
||||||
|
help='Optional YAML configuration file to merge into the generated configuration, useful for upgrading your configuration',
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-d',
|
'-d',
|
||||||
'--destination',
|
'--destination',
|
||||||
dest='destination_filename',
|
dest='destination_filename',
|
||||||
default=DEFAULT_DESTINATION_CONFIG_FILENAME,
|
default=DEFAULT_DESTINATION_CONFIG_FILENAME,
|
||||||
help='Destination YAML configuration filename. Default: {}'.format(
|
help='Destination YAML configuration file. Default: {}'.format(
|
||||||
DEFAULT_DESTINATION_CONFIG_FILENAME
|
DEFAULT_DESTINATION_CONFIG_FILENAME
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -30,11 +36,21 @@ def main(): # pragma: no cover
|
||||||
args = parse_arguments(*sys.argv[1:])
|
args = parse_arguments(*sys.argv[1:])
|
||||||
|
|
||||||
generate.generate_sample_configuration(
|
generate.generate_sample_configuration(
|
||||||
args.destination_filename, validate.schema_filename()
|
args.source_filename, args.destination_filename, validate.schema_filename()
|
||||||
)
|
)
|
||||||
|
|
||||||
print('Generated a sample configuration file at {}.'.format(args.destination_filename))
|
print('Generated a sample configuration file at {}.'.format(args.destination_filename))
|
||||||
print()
|
print()
|
||||||
|
if args.source_filename:
|
||||||
|
print(
|
||||||
|
'Merged in the contents of configuration file at {}.'.format(args.source_filename)
|
||||||
|
)
|
||||||
|
print('To review the changes made, run:')
|
||||||
|
print()
|
||||||
|
print(
|
||||||
|
' diff --unified {} {}'.format(args.source_filename, args.destination_filename)
|
||||||
|
)
|
||||||
|
print()
|
||||||
print('Please edit the file to suit your needs. The values are representative.')
|
print('Please edit the file to suit your needs. The values are representative.')
|
||||||
print('All fields are optional except where indicated.')
|
print('All fields are optional except where indicated.')
|
||||||
print()
|
print()
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
|
import collections
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from ruamel import yaml
|
from ruamel import yaml
|
||||||
|
|
||||||
|
from borgmatic.config import load
|
||||||
|
|
||||||
INDENT = 4
|
INDENT = 4
|
||||||
SEQUENCE_INDENT = 2
|
SEQUENCE_INDENT = 2
|
||||||
|
|
||||||
|
@ -40,8 +43,8 @@ def _schema_to_sample_configuration(schema, level=0, parent_is_sequence=False):
|
||||||
elif 'map' in schema:
|
elif 'map' in schema:
|
||||||
config = yaml.comments.CommentedMap(
|
config = yaml.comments.CommentedMap(
|
||||||
[
|
[
|
||||||
(section_name, _schema_to_sample_configuration(section_schema, level + 1))
|
(field_name, _schema_to_sample_configuration(sub_schema, level + 1))
|
||||||
for section_name, section_schema in schema['map'].items()
|
for field_name, sub_schema in schema['map'].items()
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
indent = (level * INDENT) + (SEQUENCE_INDENT if parent_is_sequence else 0)
|
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:]))
|
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):
|
def _comment_out_optional_configuration(rendered_config):
|
||||||
'''
|
'''
|
||||||
Post-process a rendered configuration string to comment out optional key/values. The idea is
|
Post-process a rendered configuration string to comment out optional key/values, as determined
|
||||||
that this prevents the user from having to comment out a bunch of configuration they don't care
|
by a sentinel in the comment before each key.
|
||||||
about to get to a minimal viable configuration file.
|
|
||||||
|
|
||||||
Ideally ruamel.yaml would support this during configuration generation, but it's not terribly
|
The idea is that the pre-commented configuration prevents the user from having to comment out a
|
||||||
easy to accomplish that way.
|
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 = []
|
lines = []
|
||||||
required = False
|
optional = False
|
||||||
|
|
||||||
for line in rendered_config.split('\n'):
|
for line in rendered_config.split('\n'):
|
||||||
key = line.strip().split(':')[0]
|
# Upon encountering an optional configuration option, commenting out lines until the next
|
||||||
|
# blank line.
|
||||||
if key in REQUIRED_SECTION_NAMES:
|
if line.strip().startswith('# {}'.format(COMMENTED_OUT_SENTINEL)):
|
||||||
lines.append(line)
|
optional = True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Upon encountering a required configuration option, skip commenting out lines until the
|
# Hit a blank line, so reset commenting.
|
||||||
# next blank line.
|
if not line.strip():
|
||||||
if key in REQUIRED_KEYS:
|
optional = False
|
||||||
required = True
|
|
||||||
elif not key:
|
|
||||||
required = 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)
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
@ -168,6 +166,11 @@ def add_comments_to_configuration_sequence(config, schema, indent=0):
|
||||||
return
|
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):
|
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
|
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
|
continue
|
||||||
|
|
||||||
field_schema = schema['map'].get(field_name, {})
|
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.
|
# No description to use? Skip it.
|
||||||
if not field_schema or not description:
|
if not field_schema or not description: # pragma: no cover
|
||||||
continue
|
continue
|
||||||
|
|
||||||
config.yaml_set_comment_before_after_key(key=field_name, before=description, indent=indent)
|
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)
|
_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
|
Given a configuration CommentedMap and a top-level field name in it, remove any "commented out"
|
||||||
format, write out a sample configuration file based on that schema.
|
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))
|
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(
|
write_configuration(
|
||||||
config_filename, _comment_out_optional_configuration(_render_configuration(config))
|
destination_filename,
|
||||||
|
_comment_out_optional_configuration(_render_configuration(destination_config)),
|
||||||
)
|
)
|
||||||
|
|
|
@ -77,9 +77,12 @@ else borgmatic won't recognize the option. Also be sure to use spaces rather
|
||||||
than tabs for indentation; YAML does not allow tabs.
|
than tabs for indentation; YAML does not allow tabs.
|
||||||
|
|
||||||
You can also get the same sample configuration file from the [configuration
|
You can also get the same sample configuration file from the [configuration
|
||||||
reference](https://torsion.org/borgmatic/docs/reference/configuration/), the authoritative set of
|
reference](https://torsion.org/borgmatic/docs/reference/configuration/), the
|
||||||
all configuration options. This is handy if borgmatic has added new options
|
authoritative set of all configuration options. This is handy if borgmatic has
|
||||||
since you originally created your configuration file.
|
added new options
|
||||||
|
since you originally created your configuration file. Also check out how to
|
||||||
|
[upgrade your
|
||||||
|
configuration](https://torsion.org/borgmatic/docs/how-to/upgrade/#upgrading-your-configuration).
|
||||||
|
|
||||||
|
|
||||||
### Encryption
|
### Encryption
|
||||||
|
@ -248,5 +251,6 @@ it.
|
||||||
* [Deal with very large backups](https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups/)
|
* [Deal with very large backups](https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups/)
|
||||||
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
|
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
|
||||||
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)
|
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)
|
||||||
|
* [Upgrade borgmatic](https://torsion.org/borgmatic/docs/how-to/upgrade/)
|
||||||
* [borgmatic configuration reference](https://torsion.org/borgmatic/docs/reference/configuration/)
|
* [borgmatic configuration reference](https://torsion.org/borgmatic/docs/reference/configuration/)
|
||||||
* [borgmatic command-line reference](https://torsion.org/borgmatic/docs/reference/command-line/)
|
* [borgmatic command-line reference](https://torsion.org/borgmatic/docs/reference/command-line/)
|
||||||
|
|
|
@ -10,7 +10,46 @@ following:
|
||||||
sudo pip3 install --user --upgrade borgmatic
|
sudo pip3 install --user --upgrade borgmatic
|
||||||
```
|
```
|
||||||
|
|
||||||
See below about special cases.
|
See below about special cases with old versions of borgmatic. Additionally, if
|
||||||
|
you installed borgmatic [without using `pip3 install
|
||||||
|
--user`](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#other-ways-to-install),
|
||||||
|
then your upgrade process may be different.
|
||||||
|
|
||||||
|
|
||||||
|
### Upgrading your configuration
|
||||||
|
|
||||||
|
The borgmatic configuration file format is almost always backwards-compatible
|
||||||
|
from release to release without any changes, but you may still want to update
|
||||||
|
your configuration file when you upgrade to take advantage of new
|
||||||
|
configuration options. This is completely optional. If you prefer, you can add
|
||||||
|
new configuration options manually.
|
||||||
|
|
||||||
|
If you do want to upgrade your configuration file to include new options, use
|
||||||
|
the `generate-borgmatic-config` script with its optional `--source` flag that
|
||||||
|
takes the path to your original configuration file. If provided with this
|
||||||
|
path, `generate-borgmatic-config` merges your original configuration into the
|
||||||
|
generated configuration file, so you get all the newest options and comments.
|
||||||
|
|
||||||
|
Here's an example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
generate-borgmatic-config --source config.yaml --destination config-new.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
New options start as commented out, so you can edit the file and decide
|
||||||
|
whether you want to use each one.
|
||||||
|
|
||||||
|
There are a few caveats to this process, however. First, when generating the
|
||||||
|
new configuration file, `generate-borgmatic-config` replaces any comments
|
||||||
|
you've written in your original configuration file with the newest generated
|
||||||
|
comments. Second, the script adds back any options you had originally deleted,
|
||||||
|
although it does so with the options commented out. And finally, any YAML
|
||||||
|
includes you've used in the source configuration get flattened out into a
|
||||||
|
single generated file.
|
||||||
|
|
||||||
|
As a safety measure, `generate-borgmatic-config` refuses to modify
|
||||||
|
configuration files in-place. So it's up to you to review the generated file
|
||||||
|
and, if desired, replace your original configuration file with it.
|
||||||
|
|
||||||
|
|
||||||
### Upgrading from borgmatic 1.0.x
|
### Upgrading from borgmatic 1.0.x
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -1,6 +1,6 @@
|
||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
VERSION = '1.4.6'
|
VERSION = '1.4.7'
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
|
|
|
@ -47,9 +47,13 @@ def test_comment_out_line_comments_twice_indented_option():
|
||||||
|
|
||||||
|
|
||||||
def test_comment_out_optional_configuration_comments_optional_config_only():
|
def test_comment_out_optional_configuration_comments_optional_config_only():
|
||||||
|
# The "# COMMENT_OUT" comment is a sentinel used to express that the following key is optional.
|
||||||
|
# It's stripped out of the final output.
|
||||||
flexmock(module)._comment_out_line = lambda line: '# ' + line
|
flexmock(module)._comment_out_line = lambda line: '# ' + line
|
||||||
config = '''
|
config = '''
|
||||||
|
# COMMENT_OUT
|
||||||
foo:
|
foo:
|
||||||
|
# COMMENT_OUT
|
||||||
bar:
|
bar:
|
||||||
- baz
|
- baz
|
||||||
- quux
|
- quux
|
||||||
|
@ -59,6 +63,8 @@ location:
|
||||||
- one
|
- one
|
||||||
- two
|
- two
|
||||||
|
|
||||||
|
# This comment should be kept.
|
||||||
|
# COMMENT_OUT
|
||||||
other: thing
|
other: thing
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
@ -68,12 +74,13 @@ location:
|
||||||
# bar:
|
# bar:
|
||||||
# - baz
|
# - baz
|
||||||
# - quux
|
# - quux
|
||||||
#
|
|
||||||
location:
|
location:
|
||||||
repositories:
|
repositories:
|
||||||
- one
|
- one
|
||||||
- two
|
- two
|
||||||
#
|
|
||||||
|
# This comment should be kept.
|
||||||
# other: thing
|
# other: thing
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
@ -142,12 +149,67 @@ def test_add_comments_to_configuration_map_does_not_raise():
|
||||||
module.add_comments_to_configuration_map(config, schema)
|
module.add_comments_to_configuration_map(config, schema)
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_comments_to_configuration_map_with_skip_first_does_not_raise():
|
||||||
|
config = module.yaml.comments.CommentedMap([('foo', 33)])
|
||||||
|
schema = {'map': {'foo': {'desc': 'Foo'}}}
|
||||||
|
|
||||||
|
module.add_comments_to_configuration_map(config, schema, skip_first=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_commented_out_sentinel_keeps_other_comments():
|
||||||
|
field_name = 'foo'
|
||||||
|
config = module.yaml.comments.CommentedMap([(field_name, 33)])
|
||||||
|
config.yaml_set_comment_before_after_key(key=field_name, before='Actual comment.\nCOMMENT_OUT')
|
||||||
|
|
||||||
|
module.remove_commented_out_sentinel(config, field_name)
|
||||||
|
|
||||||
|
comments = config.ca.items[field_name][module.RUAMEL_YAML_COMMENTS_INDEX]
|
||||||
|
assert len(comments) == 1
|
||||||
|
assert comments[0].value == '# Actual comment.\n'
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_commented_out_sentinel_without_sentinel_keeps_other_comments():
|
||||||
|
field_name = 'foo'
|
||||||
|
config = module.yaml.comments.CommentedMap([(field_name, 33)])
|
||||||
|
config.yaml_set_comment_before_after_key(key=field_name, before='Actual comment.')
|
||||||
|
|
||||||
|
module.remove_commented_out_sentinel(config, field_name)
|
||||||
|
|
||||||
|
comments = config.ca.items[field_name][module.RUAMEL_YAML_COMMENTS_INDEX]
|
||||||
|
assert len(comments) == 1
|
||||||
|
assert comments[0].value == '# Actual comment.\n'
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_commented_out_sentinel_on_unknown_field_does_not_raise():
|
||||||
|
field_name = 'foo'
|
||||||
|
config = module.yaml.comments.CommentedMap([(field_name, 33)])
|
||||||
|
config.yaml_set_comment_before_after_key(key=field_name, before='Actual comment.')
|
||||||
|
|
||||||
|
module.remove_commented_out_sentinel(config, 'unknown')
|
||||||
|
|
||||||
|
|
||||||
def test_generate_sample_configuration_does_not_raise():
|
def test_generate_sample_configuration_does_not_raise():
|
||||||
builtins = flexmock(sys.modules['builtins'])
|
builtins = flexmock(sys.modules['builtins'])
|
||||||
builtins.should_receive('open').with_args('schema.yaml').and_return('')
|
builtins.should_receive('open').with_args('schema.yaml').and_return('')
|
||||||
|
flexmock(module.yaml).should_receive('round_trip_load')
|
||||||
flexmock(module).should_receive('_schema_to_sample_configuration')
|
flexmock(module).should_receive('_schema_to_sample_configuration')
|
||||||
|
flexmock(module).should_receive('merge_source_configuration_into_destination')
|
||||||
flexmock(module).should_receive('_render_configuration')
|
flexmock(module).should_receive('_render_configuration')
|
||||||
flexmock(module).should_receive('_comment_out_optional_configuration')
|
flexmock(module).should_receive('_comment_out_optional_configuration')
|
||||||
flexmock(module).should_receive('write_configuration')
|
flexmock(module).should_receive('write_configuration')
|
||||||
|
|
||||||
module.generate_sample_configuration('config.yaml', 'schema.yaml')
|
module.generate_sample_configuration(None, 'dest.yaml', 'schema.yaml')
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_sample_configuration_with_source_filename_does_not_raise():
|
||||||
|
builtins = flexmock(sys.modules['builtins'])
|
||||||
|
builtins.should_receive('open').with_args('schema.yaml').and_return('')
|
||||||
|
flexmock(module.yaml).should_receive('round_trip_load')
|
||||||
|
flexmock(module.load).should_receive('load_configuration')
|
||||||
|
flexmock(module).should_receive('_schema_to_sample_configuration')
|
||||||
|
flexmock(module).should_receive('merge_source_configuration_into_destination')
|
||||||
|
flexmock(module).should_receive('_render_configuration')
|
||||||
|
flexmock(module).should_receive('_comment_out_optional_configuration')
|
||||||
|
flexmock(module).should_receive('write_configuration')
|
||||||
|
|
||||||
|
module.generate_sample_configuration('source.yaml', 'dest.yaml', 'schema.yaml')
|
||||||
|
|
|
@ -21,6 +21,7 @@ def test_convert_section_generates_integer_value_for_integer_type_in_schema():
|
||||||
|
|
||||||
def test_convert_legacy_parsed_config_transforms_source_config_to_mapping():
|
def test_convert_legacy_parsed_config_transforms_source_config_to_mapping():
|
||||||
flexmock(module.yaml.comments).should_receive('CommentedMap').replace_with(OrderedDict)
|
flexmock(module.yaml.comments).should_receive('CommentedMap').replace_with(OrderedDict)
|
||||||
|
flexmock(module.generate).should_receive('add_comments_to_configuration_map')
|
||||||
source_config = Parsed_config(
|
source_config = Parsed_config(
|
||||||
location=OrderedDict([('source_directories', '/home'), ('repository', 'hostname.borg')]),
|
location=OrderedDict([('source_directories', '/home'), ('repository', 'hostname.borg')]),
|
||||||
storage=OrderedDict([('encryption_passphrase', 'supersecret')]),
|
storage=OrderedDict([('encryption_passphrase', 'supersecret')]),
|
||||||
|
@ -53,6 +54,7 @@ def test_convert_legacy_parsed_config_transforms_source_config_to_mapping():
|
||||||
|
|
||||||
def test_convert_legacy_parsed_config_splits_space_separated_values():
|
def test_convert_legacy_parsed_config_splits_space_separated_values():
|
||||||
flexmock(module.yaml.comments).should_receive('CommentedMap').replace_with(OrderedDict)
|
flexmock(module.yaml.comments).should_receive('CommentedMap').replace_with(OrderedDict)
|
||||||
|
flexmock(module.generate).should_receive('add_comments_to_configuration_map')
|
||||||
source_config = Parsed_config(
|
source_config = Parsed_config(
|
||||||
location=OrderedDict(
|
location=OrderedDict(
|
||||||
[('source_directories', '/home /etc'), ('repository', 'hostname.borg')]
|
[('source_directories', '/home /etc'), ('repository', 'hostname.borg')]
|
||||||
|
|
|
@ -51,6 +51,7 @@ def test_schema_to_sample_configuration_generates_config_sequence_of_strings_wit
|
||||||
def test_schema_to_sample_configuration_generates_config_sequence_of_maps_with_examples():
|
def test_schema_to_sample_configuration_generates_config_sequence_of_maps_with_examples():
|
||||||
flexmock(module.yaml.comments).should_receive('CommentedSeq').replace_with(list)
|
flexmock(module.yaml.comments).should_receive('CommentedSeq').replace_with(list)
|
||||||
flexmock(module).should_receive('add_comments_to_configuration_sequence')
|
flexmock(module).should_receive('add_comments_to_configuration_sequence')
|
||||||
|
flexmock(module).should_receive('add_comments_to_configuration_map')
|
||||||
schema = {
|
schema = {
|
||||||
'seq': [
|
'seq': [
|
||||||
{
|
{
|
||||||
|
@ -71,3 +72,54 @@ def test_schema_to_sample_configuration_with_unsupported_schema_raises():
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
module._schema_to_sample_configuration(schema)
|
module._schema_to_sample_configuration(schema)
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_source_configuration_into_destination_inserts_map_fields():
|
||||||
|
destination_config = {'foo': 'dest1', 'bar': 'dest2'}
|
||||||
|
source_config = {'foo': 'source1', 'baz': 'source2'}
|
||||||
|
flexmock(module).should_receive('remove_commented_out_sentinel')
|
||||||
|
flexmock(module).should_receive('yaml.comments.CommentedSeq').replace_with(list)
|
||||||
|
|
||||||
|
module.merge_source_configuration_into_destination(destination_config, source_config)
|
||||||
|
|
||||||
|
assert destination_config == {'foo': 'source1', 'bar': 'dest2', 'baz': 'source2'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_source_configuration_into_destination_inserts_nested_map_fields():
|
||||||
|
destination_config = {'foo': {'first': 'dest1', 'second': 'dest2'}, 'bar': 'dest3'}
|
||||||
|
source_config = {'foo': {'first': 'source1'}}
|
||||||
|
flexmock(module).should_receive('remove_commented_out_sentinel')
|
||||||
|
flexmock(module).should_receive('yaml.comments.CommentedSeq').replace_with(list)
|
||||||
|
|
||||||
|
module.merge_source_configuration_into_destination(destination_config, source_config)
|
||||||
|
|
||||||
|
assert destination_config == {'foo': {'first': 'source1', 'second': 'dest2'}, 'bar': 'dest3'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_source_configuration_into_destination_inserts_sequence_fields():
|
||||||
|
destination_config = {'foo': ['dest1', 'dest2'], 'bar': ['dest3'], 'baz': ['dest4']}
|
||||||
|
source_config = {'foo': ['source1'], 'bar': ['source2', 'source3']}
|
||||||
|
flexmock(module).should_receive('remove_commented_out_sentinel')
|
||||||
|
flexmock(module).should_receive('yaml.comments.CommentedSeq').replace_with(list)
|
||||||
|
|
||||||
|
module.merge_source_configuration_into_destination(destination_config, source_config)
|
||||||
|
|
||||||
|
assert destination_config == {
|
||||||
|
'foo': ['source1'],
|
||||||
|
'bar': ['source2', 'source3'],
|
||||||
|
'baz': ['dest4'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_source_configuration_into_destination_inserts_sequence_of_maps():
|
||||||
|
destination_config = {'foo': [{'first': 'dest1', 'second': 'dest2'}], 'bar': 'dest3'}
|
||||||
|
source_config = {'foo': [{'first': 'source1'}, {'other': 'source2'}]}
|
||||||
|
flexmock(module).should_receive('remove_commented_out_sentinel')
|
||||||
|
flexmock(module).should_receive('yaml.comments.CommentedSeq').replace_with(list)
|
||||||
|
|
||||||
|
module.merge_source_configuration_into_destination(destination_config, source_config)
|
||||||
|
|
||||||
|
assert destination_config == {
|
||||||
|
'foo': [{'first': 'source1', 'second': 'dest2'}, {'other': 'source2'}],
|
||||||
|
'bar': 'dest3',
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue