Basic YAML generating / validating / converting to.
This commit is contained in:
parent
bff6980eee
commit
745de200df
16 changed files with 327 additions and 40 deletions
5
NEWS
5
NEWS
|
@ -1,8 +1,9 @@
|
||||||
1.1.0
|
1.1.0.dev0
|
||||||
|
|
||||||
|
* Switched config file format to YAML. Run convert-borgmatic-config to upgrade.
|
||||||
|
* Dropped Python 2 support. Now Python 3 only.
|
||||||
* #18: Fix for README mention of sample files not included in package.
|
* #18: Fix for README mention of sample files not included in package.
|
||||||
* #22: Sample files for triggering borgmatic from a systemd timer.
|
* #22: Sample files for triggering borgmatic from a systemd timer.
|
||||||
* Dropped Python 2 support. Now Python 3 only.
|
|
||||||
* Added logo.
|
* Added logo.
|
||||||
|
|
||||||
1.0.3
|
1.0.3
|
||||||
|
|
0
borgmatic/commands/__init__.py
Normal file
0
borgmatic/commands/__init__.py
Normal file
|
@ -5,7 +5,7 @@ from subprocess import CalledProcessError
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from borgmatic import borg
|
from borgmatic import borg
|
||||||
from borgmatic.config.yaml import parse_configuration, schema_filename
|
from borgmatic.config.validate import parse_configuration, schema_filename
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CONFIG_FILENAME = '/etc/borgmatic/config.yaml'
|
DEFAULT_CONFIG_FILENAME = '/etc/borgmatic/config.yaml'
|
||||||
|
@ -14,9 +14,8 @@ DEFAULT_EXCLUDES_FILENAME = '/etc/borgmatic/excludes'
|
||||||
|
|
||||||
def parse_arguments(*arguments):
|
def parse_arguments(*arguments):
|
||||||
'''
|
'''
|
||||||
Given the name of the command with which this script was invoked and command-line arguments,
|
Given command-line arguments with which this script was invoked, parse the arguments and return
|
||||||
parse the arguments and return them as an ArgumentParser instance. Use the command name to
|
them as an ArgumentParser instance.
|
||||||
determine the default configuration and excludes paths.
|
|
||||||
'''
|
'''
|
||||||
parser = ArgumentParser()
|
parser = ArgumentParser()
|
||||||
parser.add_argument(
|
parser.add_argument(
|
54
borgmatic/commands/convert_config.py
Normal file
54
borgmatic/commands/convert_config.py
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
from __future__ import print_function
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
import os
|
||||||
|
from subprocess import CalledProcessError
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from ruamel import yaml
|
||||||
|
|
||||||
|
from borgmatic import borg
|
||||||
|
from borgmatic.config import convert, generate, legacy, validate
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_SOURCE_CONFIG_FILENAME = '/etc/borgmatic/config'
|
||||||
|
DEFAULT_SOURCE_EXCLUDES_FILENAME = '/etc/borgmatic/excludes'
|
||||||
|
DEFAULT_DESTINATION_CONFIG_FILENAME = '/etc/borgmatic/config.yaml'
|
||||||
|
|
||||||
|
|
||||||
|
def parse_arguments(*arguments):
|
||||||
|
'''
|
||||||
|
Given command-line arguments with which this script was invoked, parse the arguments and return
|
||||||
|
them as an ArgumentParser instance.
|
||||||
|
'''
|
||||||
|
parser = ArgumentParser(description='Convert a legacy INI-style borgmatic configuration file to YAML. Does not preserve comments.')
|
||||||
|
parser.add_argument(
|
||||||
|
'-s', '--source',
|
||||||
|
dest='source_filename',
|
||||||
|
default=DEFAULT_SOURCE_CONFIG_FILENAME,
|
||||||
|
help='Source INI-style configuration filename. Default: {}'.format(DEFAULT_SOURCE_CONFIG_FILENAME),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-d', '--destination',
|
||||||
|
dest='destination_filename',
|
||||||
|
default=DEFAULT_DESTINATION_CONFIG_FILENAME,
|
||||||
|
help='Destination YAML configuration filename. Default: {}'.format(DEFAULT_DESTINATION_CONFIG_FILENAME),
|
||||||
|
)
|
||||||
|
|
||||||
|
return parser.parse_args(arguments)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
args = parse_arguments(*sys.argv[1:])
|
||||||
|
source_config = legacy.parse_configuration(args.source_filename, legacy.CONFIG_FORMAT)
|
||||||
|
schema = yaml.round_trip_load(open(validate.schema_filename()).read())
|
||||||
|
|
||||||
|
destination_config = convert.convert_legacy_parsed_config(source_config, schema)
|
||||||
|
|
||||||
|
generate.write_configuration(args.destination_filename, destination_config)
|
||||||
|
|
||||||
|
# TODO: As a backstop, check that the written config can actually be read and parsed, and
|
||||||
|
# that it matches the destination config data structure that was written.
|
||||||
|
except (ValueError, OSError) as error:
|
||||||
|
print(error, file=sys.stderr)
|
||||||
|
sys.exit(1)
|
41
borgmatic/config/convert.py
Normal file
41
borgmatic/config/convert.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
from ruamel import yaml
|
||||||
|
|
||||||
|
from borgmatic.config import generate
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_section(source_section_config, section_schema):
|
||||||
|
'''
|
||||||
|
Given a legacy Parsed_config instance for a single section, convert it to its corresponding
|
||||||
|
yaml.comments.CommentedMap representation in preparation for actual serialization to YAML.
|
||||||
|
|
||||||
|
Additionally, use the section schema as a source of helpful comments to include within the
|
||||||
|
returned CommentedMap.
|
||||||
|
'''
|
||||||
|
destination_section_config = yaml.comments.CommentedMap(source_section_config)
|
||||||
|
generate.add_comments_to_configuration(destination_section_config, section_schema, indent=generate.INDENT)
|
||||||
|
|
||||||
|
return destination_section_config
|
||||||
|
|
||||||
|
|
||||||
|
def convert_legacy_parsed_config(source_config, schema):
|
||||||
|
'''
|
||||||
|
Given a legacy Parsed_config instance loaded from an INI-style config file, convert it to its
|
||||||
|
corresponding yaml.comments.CommentedMap representation in preparation for actual serialization
|
||||||
|
to YAML.
|
||||||
|
|
||||||
|
Additionally, use the given schema as a source of helpful comments to include within the
|
||||||
|
returned CommentedMap.
|
||||||
|
'''
|
||||||
|
destination_config = yaml.comments.CommentedMap([
|
||||||
|
(section_name, _convert_section(section_config, schema['map'][section_name]))
|
||||||
|
for section_name, section_config in source_config._asdict().items()
|
||||||
|
])
|
||||||
|
|
||||||
|
destination_config['location']['source_directories'] = source_config.location['source_directories'].split(' ')
|
||||||
|
|
||||||
|
if source_config.consistency['checks']:
|
||||||
|
destination_config['consistency']['checks'] = source_config.consistency['checks'].split(' ')
|
||||||
|
|
||||||
|
generate.add_comments_to_configuration(destination_config, schema)
|
||||||
|
|
||||||
|
return destination_config
|
90
borgmatic/config/generate.py
Normal file
90
borgmatic/config/generate.py
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from ruamel import yaml
|
||||||
|
|
||||||
|
|
||||||
|
INDENT = 4
|
||||||
|
|
||||||
|
|
||||||
|
def write_configuration(config_filename, config):
|
||||||
|
'''
|
||||||
|
Given a target config filename and a config data structure of nested OrderedDicts, write out the
|
||||||
|
config to file as YAML.
|
||||||
|
'''
|
||||||
|
with open(config_filename, 'w') as config_file:
|
||||||
|
config_file.write(yaml.round_trip_dump(config, indent=INDENT, block_seq_indent=INDENT))
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
field and its comments.
|
||||||
|
'''
|
||||||
|
config.ca.items[field_name][1].insert(
|
||||||
|
0,
|
||||||
|
yaml.tokens.CommentToken('\n', yaml.error.CommentMark(0), None),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_comments_to_configuration(config, schema, indent=0):
|
||||||
|
'''
|
||||||
|
Using descriptions from a schema as a source, add those descriptions as comments to the given
|
||||||
|
config before each field. This function only adds comments for the top-most config map level.
|
||||||
|
Indent the comment the given number of characters.
|
||||||
|
'''
|
||||||
|
for index, field_name in enumerate(config.keys()):
|
||||||
|
field_schema = schema['map'].get(field_name, {})
|
||||||
|
description = field_schema.get('desc')
|
||||||
|
|
||||||
|
# No description to use? Skip it.
|
||||||
|
if not schema or not description:
|
||||||
|
continue
|
||||||
|
|
||||||
|
config.yaml_set_comment_before_after_key(
|
||||||
|
key=field_name,
|
||||||
|
before=description,
|
||||||
|
indent=indent,
|
||||||
|
)
|
||||||
|
if index > 0:
|
||||||
|
_insert_newline_before_comment(config, field_name)
|
||||||
|
|
||||||
|
|
||||||
|
def _section_schema_to_sample_configuration(section_schema):
|
||||||
|
'''
|
||||||
|
Given the schema for a particular config section, generate and return sample config for that
|
||||||
|
section. Include comments for each field based on the schema "desc" description.
|
||||||
|
'''
|
||||||
|
section_config = yaml.comments.CommentedMap([
|
||||||
|
(field_name, field_schema['example'])
|
||||||
|
for field_name, field_schema in section_schema['map'].items()
|
||||||
|
])
|
||||||
|
|
||||||
|
add_comments_to_configuration(section_config, section_schema, indent=INDENT)
|
||||||
|
|
||||||
|
return section_config
|
||||||
|
|
||||||
|
|
||||||
|
def _schema_to_sample_configuration(schema):
|
||||||
|
'''
|
||||||
|
Given a loaded configuration schema, generate and return sample config for it. Include comments
|
||||||
|
for each section based on the schema "desc" description.
|
||||||
|
'''
|
||||||
|
config = yaml.comments.CommentedMap([
|
||||||
|
(section_name, _section_schema_to_sample_configuration(section_schema))
|
||||||
|
for section_name, section_schema in schema['map'].items()
|
||||||
|
])
|
||||||
|
|
||||||
|
add_comments_to_configuration(config, schema)
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def generate_sample_configuration(config_filename, schema_filename):
|
||||||
|
'''
|
||||||
|
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.
|
||||||
|
'''
|
||||||
|
schema = yaml.round_trip_load(open(schema_filename))
|
||||||
|
config = _schema_to_sample_configuration(schema)
|
||||||
|
|
||||||
|
write_configuration(config_filename, config)
|
|
@ -1,48 +1,110 @@
|
||||||
|
name: Borgmatic configuration file schema
|
||||||
map:
|
map:
|
||||||
location:
|
location:
|
||||||
|
desc: |
|
||||||
|
Where to look for files to backup, and where to store those backups. See
|
||||||
|
https://borgbackup.readthedocs.io/en/stable/quickstart.html and
|
||||||
|
https://borgbackup.readthedocs.io/en/stable/usage.html#borg-create for details.
|
||||||
required: True
|
required: True
|
||||||
map:
|
map:
|
||||||
source_directories:
|
source_directories:
|
||||||
required: True
|
required: True
|
||||||
seq:
|
seq:
|
||||||
- type: scalar
|
- type: scalar
|
||||||
|
desc: List of source directories to backup. Globs are expanded.
|
||||||
|
example:
|
||||||
|
- /home
|
||||||
|
- /etc
|
||||||
|
- /var/log/syslog*
|
||||||
one_file_system:
|
one_file_system:
|
||||||
type: bool
|
type: bool
|
||||||
|
desc: Stay in same file system (do not cross mount points).
|
||||||
|
example: yes
|
||||||
remote_path:
|
remote_path:
|
||||||
type: scalar
|
type: scalar
|
||||||
|
desc: Alternate Borg remote executable. Defaults to "borg".
|
||||||
|
example: borg1
|
||||||
repository:
|
repository:
|
||||||
required: True
|
required: True
|
||||||
type: scalar
|
type: scalar
|
||||||
|
desc: Path to local or remote repository.
|
||||||
|
example: user@backupserver:sourcehostname.borg
|
||||||
storage:
|
storage:
|
||||||
|
desc: |
|
||||||
|
Repository storage options. See
|
||||||
|
https://borgbackup.readthedocs.io/en/stable/usage.html#borg-create and
|
||||||
|
https://borgbackup.readthedocs.io/en/stable/usage.html#environment-variables for details.
|
||||||
map:
|
map:
|
||||||
encryption_passphrase:
|
encryption_passphrase:
|
||||||
type: scalar
|
type: scalar
|
||||||
|
desc: |
|
||||||
|
Passphrase to unlock the encryption key with. Only use on repositories that were
|
||||||
|
initialized with passphrase/repokey encryption. Quote the value if it contains
|
||||||
|
punctuation, so it parses correctly. And backslash any quote or backslash
|
||||||
|
literals as well.
|
||||||
|
example: "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
|
||||||
compression:
|
compression:
|
||||||
type: scalar
|
type: scalar
|
||||||
|
desc: |
|
||||||
|
Type of compression to use when creating archives. See
|
||||||
|
https://borgbackup.readthedocs.org/en/stable/usage.html#borg-create for details.
|
||||||
|
Defaults to no compression.
|
||||||
|
example: lz4
|
||||||
umask:
|
umask:
|
||||||
type: scalar
|
type: scalar
|
||||||
|
desc: Umask to be used for borg create.
|
||||||
|
example: 0077
|
||||||
retention:
|
retention:
|
||||||
|
desc: |
|
||||||
|
Retention policy for how many backups to keep in each category. See
|
||||||
|
https://borgbackup.readthedocs.org/en/stable/usage.html#borg-prune for details.
|
||||||
map:
|
map:
|
||||||
keep_within:
|
keep_within:
|
||||||
type: scalar
|
type: scalar
|
||||||
|
desc: Keep all archives within this time interval.
|
||||||
|
example: 3H
|
||||||
keep_hourly:
|
keep_hourly:
|
||||||
type: int
|
type: int
|
||||||
|
desc: Number of hourly archives to keep.
|
||||||
|
example: 24
|
||||||
keep_daily:
|
keep_daily:
|
||||||
type: int
|
type: int
|
||||||
|
desc: Number of daily archives to keep.
|
||||||
|
example: 7
|
||||||
keep_weekly:
|
keep_weekly:
|
||||||
type: int
|
type: int
|
||||||
|
desc: Number of weekly archives to keep.
|
||||||
|
example: 4
|
||||||
keep_monthly:
|
keep_monthly:
|
||||||
type: int
|
type: int
|
||||||
|
desc: Number of monthly archives to keep.
|
||||||
|
example: 6
|
||||||
keep_yearly:
|
keep_yearly:
|
||||||
type: int
|
type: int
|
||||||
|
desc: Number of yearly archives to keep.
|
||||||
|
example: 1
|
||||||
prefix:
|
prefix:
|
||||||
type: scalar
|
type: scalar
|
||||||
|
desc: When pruning, only consider archive names starting with this prefix.
|
||||||
|
example: sourcehostname
|
||||||
consistency:
|
consistency:
|
||||||
|
desc: |
|
||||||
|
Consistency checks to run after backups. See
|
||||||
|
https://borgbackup.readthedocs.org/en/stable/usage.html#borg-check for details.
|
||||||
map:
|
map:
|
||||||
checks:
|
checks:
|
||||||
seq:
|
seq:
|
||||||
- type: str
|
- type: str
|
||||||
enum: ['repository', 'archives', 'disabled']
|
enum: ['repository', 'archives', 'disabled']
|
||||||
unique: True
|
unique: True
|
||||||
|
desc: |
|
||||||
|
List of consistency checks to run: "repository", "archives", or both. Defaults
|
||||||
|
to both. Set to "disabled" to disable all consistency checks. See
|
||||||
|
https://borgbackup.readthedocs.org/en/stable/usage.html#borg-check for details.
|
||||||
|
example:
|
||||||
|
- repository
|
||||||
|
- archives
|
||||||
check_last:
|
check_last:
|
||||||
type: int
|
type: int
|
||||||
|
desc: Restrict the number of checked archives to the last n.
|
||||||
|
example: 3
|
||||||
|
|
|
@ -5,7 +5,7 @@ import warnings
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
import pykwalify.core
|
import pykwalify.core
|
||||||
import pykwalify.errors
|
import pykwalify.errors
|
||||||
import ruamel.yaml.error
|
from ruamel import yaml
|
||||||
|
|
||||||
|
|
||||||
def schema_filename():
|
def schema_filename():
|
||||||
|
@ -38,20 +38,18 @@ def parse_configuration(config_filename, schema_filename):
|
||||||
Raise FileNotFoundError if the file does not exist, PermissionError if the user does not
|
Raise FileNotFoundError if the file does not exist, PermissionError if the user does not
|
||||||
have permissions to read the file, or Validation_error if the config does not match the schema.
|
have permissions to read the file, or Validation_error if the config does not match the schema.
|
||||||
'''
|
'''
|
||||||
warnings.simplefilter('ignore', ruamel.yaml.error.UnsafeLoaderWarning)
|
|
||||||
logging.getLogger('pykwalify').setLevel(logging.CRITICAL)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
validator = pykwalify.core.Core(source_file=config_filename, schema_files=[schema_filename])
|
schema = yaml.round_trip_load(open(schema_filename))
|
||||||
except pykwalify.errors.CoreError as error:
|
except yaml.error.YAMLError as error:
|
||||||
if 'do not exists on disk' in str(error):
|
raise Validation_error(config_filename, (str(error),))
|
||||||
raise FileNotFoundError("No such file or directory: '{}'".format(config_filename))
|
|
||||||
if 'Unable to load any data' in str(error):
|
|
||||||
# If the YAML file has a syntax error, pykwalify's exception is particularly unhelpful.
|
|
||||||
# So reach back to the originating exception from ruamel.yaml for something more useful.
|
|
||||||
raise Validation_error(config_filename, (error.__context__,))
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
# pykwalify gets angry if the example field is not a string. So rather than bend to its will,
|
||||||
|
# simply remove all examples before passing the schema to pykwalify.
|
||||||
|
for section_name, section_schema in schema['map'].items():
|
||||||
|
for field_name, field_schema in section_schema['map'].items():
|
||||||
|
field_schema.pop('example')
|
||||||
|
|
||||||
|
validator = pykwalify.core.Core(source_file=config_filename, schema_data=schema)
|
||||||
parsed_result = validator.validate(raise_exception=False)
|
parsed_result = validator.validate(raise_exception=False)
|
||||||
|
|
||||||
if validator.validation_errors:
|
if validator.validation_errors:
|
||||||
|
@ -73,12 +71,3 @@ def display_validation_error(validation_error):
|
||||||
|
|
||||||
for error in validation_error.error_messages:
|
for error in validation_error.error_messages:
|
||||||
print(error, file=sys.stderr)
|
print(error, file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
# FOR TESTING
|
|
||||||
if __name__ == '__main__':
|
|
||||||
try:
|
|
||||||
configuration = parse_configuration('sample/config.yaml', schema_filename())
|
|
||||||
print(configuration)
|
|
||||||
except Validation_error as error:
|
|
||||||
display_validation_error(error)
|
|
0
borgmatic/tests/integration/commands/__init__.py
Normal file
0
borgmatic/tests/integration/commands/__init__.py
Normal file
|
@ -4,7 +4,7 @@ import sys
|
||||||
from flexmock import flexmock
|
from flexmock import flexmock
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from borgmatic import command as module
|
from borgmatic.commands import borgmatic as module
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_no_arguments_uses_defaults():
|
def test_parse_arguments_with_no_arguments_uses_defaults():
|
|
@ -6,7 +6,7 @@ import os
|
||||||
from flexmock import flexmock
|
from flexmock import flexmock
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from borgmatic.config import yaml as module
|
from borgmatic.config import validate as module
|
||||||
|
|
||||||
|
|
||||||
def test_schema_filename_returns_plausable_path():
|
def test_schema_filename_returns_plausable_path():
|
||||||
|
@ -18,13 +18,13 @@ def test_schema_filename_returns_plausable_path():
|
||||||
def mock_config_and_schema(config_yaml):
|
def mock_config_and_schema(config_yaml):
|
||||||
'''
|
'''
|
||||||
Set up mocks for the config config YAML string and the default schema so that pykwalify consumes
|
Set up mocks for the config config YAML string and the default schema so that pykwalify consumes
|
||||||
them when parsing the configuration. This is a little brittle in that it's relying on pykwalify
|
them when parsing the configuration. This is a little brittle in that it's relying on the code
|
||||||
to open() the respective files in a particular order.
|
under test to open() the respective files in a particular order.
|
||||||
'''
|
'''
|
||||||
config_stream = io.StringIO(config_yaml)
|
|
||||||
schema_stream = open(module.schema_filename())
|
schema_stream = open(module.schema_filename())
|
||||||
|
config_stream = io.StringIO(config_yaml)
|
||||||
builtins = flexmock(sys.modules['builtins']).should_call('open').mock
|
builtins = flexmock(sys.modules['builtins']).should_call('open').mock
|
||||||
builtins.should_receive('open').and_return(config_stream).and_return(schema_stream)
|
builtins.should_receive('open').and_return(schema_stream).and_return(config_stream)
|
||||||
flexmock(os.path).should_receive('exists').and_return(True)
|
flexmock(os.path).should_receive('exists').and_return(True)
|
||||||
|
|
||||||
|
|
||||||
|
@ -87,7 +87,8 @@ def test_parse_configuration_raises_for_missing_config_file():
|
||||||
|
|
||||||
def test_parse_configuration_raises_for_missing_schema_file():
|
def test_parse_configuration_raises_for_missing_schema_file():
|
||||||
mock_config_and_schema('')
|
mock_config_and_schema('')
|
||||||
flexmock(os.path).should_receive('exists').with_args('schema.yaml').and_return(False)
|
builtins = flexmock(sys.modules['builtins'])
|
||||||
|
builtins.should_receive('open').with_args('schema.yaml').and_raise(FileNotFoundError)
|
||||||
|
|
||||||
with pytest.raises(FileNotFoundError):
|
with pytest.raises(FileNotFoundError):
|
||||||
module.parse_configuration('config.yaml', 'schema.yaml')
|
module.parse_configuration('config.yaml', 'schema.yaml')
|
44
borgmatic/tests/unit/config/test_convert.py
Normal file
44
borgmatic/tests/unit/config/test_convert.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
from collections import defaultdict, OrderedDict, namedtuple
|
||||||
|
|
||||||
|
from borgmatic.config import convert as module
|
||||||
|
|
||||||
|
|
||||||
|
Parsed_config = namedtuple('Parsed_config', ('location', 'storage', 'retention', 'consistency'))
|
||||||
|
|
||||||
|
|
||||||
|
def test_convert_legacy_parsed_config_transforms_source_config_to_mapping():
|
||||||
|
source_config = Parsed_config(
|
||||||
|
location=OrderedDict([('source_directories', '/home'), ('repository', 'hostname.borg')]),
|
||||||
|
storage=OrderedDict([('encryption_passphrase', 'supersecret')]),
|
||||||
|
retention=OrderedDict([('keep_daily', 7)]),
|
||||||
|
consistency=OrderedDict([('checks', 'repository')]),
|
||||||
|
)
|
||||||
|
schema = {'map': defaultdict(lambda: {'map': {}})}
|
||||||
|
|
||||||
|
destination_config = module.convert_legacy_parsed_config(source_config, schema)
|
||||||
|
|
||||||
|
assert destination_config == OrderedDict([
|
||||||
|
('location', OrderedDict([('source_directories', ['/home']), ('repository', 'hostname.borg')])),
|
||||||
|
('storage', OrderedDict([('encryption_passphrase', 'supersecret')])),
|
||||||
|
('retention', OrderedDict([('keep_daily', 7)])),
|
||||||
|
('consistency', OrderedDict([('checks', ['repository'])])),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def test_convert_legacy_parsed_config_splits_space_separated_values():
|
||||||
|
source_config = Parsed_config(
|
||||||
|
location=OrderedDict([('source_directories', '/home /etc')]),
|
||||||
|
storage=OrderedDict(),
|
||||||
|
retention=OrderedDict(),
|
||||||
|
consistency=OrderedDict([('checks', 'repository archives')]),
|
||||||
|
)
|
||||||
|
schema = {'map': defaultdict(lambda: {'map': {}})}
|
||||||
|
|
||||||
|
destination_config = module.convert_legacy_parsed_config(source_config, schema)
|
||||||
|
|
||||||
|
assert destination_config == OrderedDict([
|
||||||
|
('location', OrderedDict([('source_directories', ['/home', '/etc'])])),
|
||||||
|
('storage', OrderedDict()),
|
||||||
|
('retention', OrderedDict()),
|
||||||
|
('consistency', OrderedDict([('checks', ['repository', 'archives'])])),
|
||||||
|
])
|
|
@ -16,8 +16,10 @@ location:
|
||||||
|
|
||||||
#storage:
|
#storage:
|
||||||
# Passphrase to unlock the encryption key with. Only use on repositories
|
# Passphrase to unlock the encryption key with. Only use on repositories
|
||||||
# that were initialized with passphrase/repokey encryption.
|
# that were initialized with passphrase/repokey encryption. Quote the value
|
||||||
#encryption_passphrase: foo
|
# if it contains punctuation so it parses correctly. And backslash any
|
||||||
|
# quote or backslash literals as well.
|
||||||
|
#encryption_passphrase: "foo"
|
||||||
|
|
||||||
# Type of compression to use when creating archives. See
|
# Type of compression to use when creating archives. See
|
||||||
# https://borgbackup.readthedocs.org/en/stable/usage.html#borg-create
|
# https://borgbackup.readthedocs.org/en/stable/usage.html#borg-create
|
||||||
|
|
5
setup.py
5
setup.py
|
@ -1,7 +1,7 @@
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
|
||||||
VERSION = '1.1.0'
|
VERSION = '1.1.0.dev0'
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
|
@ -24,7 +24,8 @@ setup(
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
'borgmatic = borgmatic.command:main',
|
'borgmatic = borgmatic.commands.borgmatic:main',
|
||||||
|
'convert-borgmatic-config = borgmatic.commands.convert_config:main',
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
obsoletes=[
|
obsoletes=[
|
||||||
|
|
|
@ -1,2 +1,5 @@
|
||||||
flexmock==0.10.2
|
flexmock==0.10.2
|
||||||
|
pykwalify==1.6.0
|
||||||
pytest==2.9.1
|
pytest==2.9.1
|
||||||
|
pytest-cov==2.5.1
|
||||||
|
ruamel.yaml==0.15.18
|
||||||
|
|
2
tox.ini
2
tox.ini
|
@ -5,4 +5,4 @@ skipsdist=True
|
||||||
[testenv]
|
[testenv]
|
||||||
usedevelop=True
|
usedevelop=True
|
||||||
deps=-rtest_requirements.txt
|
deps=-rtest_requirements.txt
|
||||||
commands = py.test borgmatic []
|
commands = py.test --cov=borgmatic borgmatic []
|
||||||
|
|
Loading…
Reference in a new issue