Remove upgrade-borgmatic-config command for upgrading borgmatic 1.1.0 INI-style configuration (#529).
This commit is contained in:
parent
6098005f5d
commit
b10aee3070
7 changed files with 7 additions and 395 deletions
2
NEWS
2
NEWS
|
@ -1,5 +1,7 @@
|
||||||
1.7.15.dev0
|
1.7.15.dev0
|
||||||
* #399: Add a documentation troubleshooting note for MySQL/MariaDB authentication errors.
|
* #399: Add a documentation troubleshooting note for MySQL/MariaDB authentication errors.
|
||||||
|
* #529: Remove upgrade-borgmatic-config command for upgrading borgmatic 1.1.0 INI-style
|
||||||
|
configuration.
|
||||||
* #697, #712: Extract borgmatic configuration from backup via "bootstrap" action—even when
|
* #697, #712: Extract borgmatic configuration from backup via "bootstrap" action—even when
|
||||||
borgmatic has no configuration yet!
|
borgmatic has no configuration yet!
|
||||||
* #669: Add sample systemd user service for running borgmatic as a non-root user.
|
* #669: Add sample systemd user service for running borgmatic as a non-root user.
|
||||||
|
|
|
@ -1,102 +0,0 @@
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import textwrap
|
|
||||||
from argparse import ArgumentParser
|
|
||||||
|
|
||||||
from ruamel import yaml
|
|
||||||
|
|
||||||
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 legacy INI-style borgmatic configuration and excludes files to a single YAML
|
|
||||||
configuration file. Note that this replaces any comments from the source files.
|
|
||||||
'''
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-s',
|
|
||||||
'--source-config',
|
|
||||||
dest='source_config_filename',
|
|
||||||
default=DEFAULT_SOURCE_CONFIG_FILENAME,
|
|
||||||
help=f'Source INI-style configuration filename. Default: {DEFAULT_SOURCE_CONFIG_FILENAME}',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-e',
|
|
||||||
'--source-excludes',
|
|
||||||
dest='source_excludes_filename',
|
|
||||||
default=DEFAULT_SOURCE_EXCLUDES_FILENAME
|
|
||||||
if os.path.exists(DEFAULT_SOURCE_EXCLUDES_FILENAME)
|
|
||||||
else None,
|
|
||||||
help='Excludes filename',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-d',
|
|
||||||
'--destination-config',
|
|
||||||
dest='destination_config_filename',
|
|
||||||
default=DEFAULT_DESTINATION_CONFIG_FILENAME,
|
|
||||||
help=f'Destination YAML configuration filename. Default: {DEFAULT_DESTINATION_CONFIG_FILENAME}',
|
|
||||||
)
|
|
||||||
|
|
||||||
return parser.parse_args(arguments)
|
|
||||||
|
|
||||||
|
|
||||||
TEXT_WRAP_CHARACTERS = 80
|
|
||||||
|
|
||||||
|
|
||||||
def display_result(args): # pragma: no cover
|
|
||||||
result_lines = textwrap.wrap(
|
|
||||||
f'Your borgmatic configuration has been upgraded. Please review the result in {args.destination_config_filename}.',
|
|
||||||
TEXT_WRAP_CHARACTERS,
|
|
||||||
)
|
|
||||||
|
|
||||||
excludes_phrase = (
|
|
||||||
f' and {args.source_excludes_filename}' if args.source_excludes_filename else ''
|
|
||||||
)
|
|
||||||
delete_lines = textwrap.wrap(
|
|
||||||
f'Once you are satisfied, you can safely delete {args.source_config_filename}{excludes_phrase}.',
|
|
||||||
TEXT_WRAP_CHARACTERS,
|
|
||||||
)
|
|
||||||
|
|
||||||
print('\n'.join(result_lines))
|
|
||||||
print()
|
|
||||||
print('\n'.join(delete_lines))
|
|
||||||
|
|
||||||
|
|
||||||
def main(): # pragma: no cover
|
|
||||||
try:
|
|
||||||
args = parse_arguments(*sys.argv[1:])
|
|
||||||
schema = yaml.round_trip_load(open(validate.schema_filename()).read())
|
|
||||||
source_config = legacy.parse_configuration(
|
|
||||||
args.source_config_filename, legacy.CONFIG_FORMAT
|
|
||||||
)
|
|
||||||
source_config_file_mode = os.stat(args.source_config_filename).st_mode
|
|
||||||
source_excludes = (
|
|
||||||
open(args.source_excludes_filename).read().splitlines()
|
|
||||||
if args.source_excludes_filename
|
|
||||||
else []
|
|
||||||
)
|
|
||||||
|
|
||||||
destination_config = convert.convert_legacy_parsed_config(
|
|
||||||
source_config, source_excludes, schema
|
|
||||||
)
|
|
||||||
|
|
||||||
generate.write_configuration(
|
|
||||||
args.destination_config_filename,
|
|
||||||
generate.render_configuration(destination_config),
|
|
||||||
mode=source_config_file_mode,
|
|
||||||
)
|
|
||||||
|
|
||||||
display_result(args)
|
|
||||||
except (ValueError, OSError) as error:
|
|
||||||
print(error, file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
|
@ -1,95 +0,0 @@
|
||||||
import os
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Where integer types exist in the given section schema, convert their values to integers.
|
|
||||||
'''
|
|
||||||
destination_section_config = yaml.comments.CommentedMap(
|
|
||||||
[
|
|
||||||
(
|
|
||||||
option_name,
|
|
||||||
int(option_value)
|
|
||||||
if section_schema['properties'].get(option_name, {}).get('type') == 'integer'
|
|
||||||
else option_value,
|
|
||||||
)
|
|
||||||
for option_name, option_value in source_section_config.items()
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
return destination_section_config
|
|
||||||
|
|
||||||
|
|
||||||
def convert_legacy_parsed_config(source_config, source_excludes, schema):
|
|
||||||
'''
|
|
||||||
Given a legacy Parsed_config instance loaded from an INI-style config file and a list of exclude
|
|
||||||
patterns, convert them to a corresponding yaml.comments.CommentedMap representation in
|
|
||||||
preparation for serialization to a single YAML config file.
|
|
||||||
|
|
||||||
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['properties'][section_name]))
|
|
||||||
for section_name, section_config in source_config._asdict().items()
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Split space-separated values into actual lists, make "repository" into a list, and merge in
|
|
||||||
# excludes.
|
|
||||||
location = destination_config['location']
|
|
||||||
location['source_directories'] = source_config.location['source_directories'].split(' ')
|
|
||||||
location['repositories'] = [location.pop('repository')]
|
|
||||||
location['exclude_patterns'] = source_excludes
|
|
||||||
|
|
||||||
if source_config.consistency.get('checks'):
|
|
||||||
destination_config['consistency']['checks'] = source_config.consistency['checks'].split(' ')
|
|
||||||
|
|
||||||
# Add comments to each section, and then add comments to the fields in each section.
|
|
||||||
generate.add_comments_to_configuration_object(destination_config, schema)
|
|
||||||
|
|
||||||
for section_name, section_config in destination_config.items():
|
|
||||||
generate.add_comments_to_configuration_object(
|
|
||||||
section_config, schema['properties'][section_name], indent=generate.INDENT
|
|
||||||
)
|
|
||||||
|
|
||||||
return destination_config
|
|
||||||
|
|
||||||
|
|
||||||
class Legacy_configuration_not_upgraded(FileNotFoundError):
|
|
||||||
def __init__(self):
|
|
||||||
super(Legacy_configuration_not_upgraded, self).__init__(
|
|
||||||
'''borgmatic changed its configuration file format in version 1.1.0 from INI-style
|
|
||||||
to YAML. This better supports validation, and has a more natural way to express
|
|
||||||
lists of values. To upgrade your existing configuration, run:
|
|
||||||
|
|
||||||
sudo upgrade-borgmatic-config
|
|
||||||
|
|
||||||
That will generate a new YAML configuration file at /etc/borgmatic/config.yaml
|
|
||||||
(by default) using the values from both your existing configuration and excludes
|
|
||||||
files. The new version of borgmatic will consume the YAML configuration file
|
|
||||||
instead of the old one.'''
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def guard_configuration_upgraded(source_config_filename, destination_config_filenames):
|
|
||||||
'''
|
|
||||||
If legacy source configuration exists but no destination upgraded configs do, raise
|
|
||||||
Legacy_configuration_not_upgraded.
|
|
||||||
|
|
||||||
The idea is that we want to alert the user about upgrading their config if they haven't already.
|
|
||||||
'''
|
|
||||||
destination_config_exists = any(
|
|
||||||
os.path.exists(filename) for filename in destination_config_filenames
|
|
||||||
)
|
|
||||||
|
|
||||||
if os.path.exists(source_config_filename) and not destination_config_exists:
|
|
||||||
raise Legacy_configuration_not_upgraded()
|
|
|
@ -61,21 +61,22 @@ and, if desired, replace your original configuration file with it.
|
||||||
borgmatic changed its configuration file format in version 1.1.0 from
|
borgmatic changed its configuration file format in version 1.1.0 from
|
||||||
INI-style to YAML. This better supports validation, and has a more natural way
|
INI-style to YAML. This better supports validation, and has a more natural way
|
||||||
to express lists of values. To upgrade your existing configuration, first
|
to express lists of values. To upgrade your existing configuration, first
|
||||||
upgrade to the new version of borgmatic.
|
upgrade to the last version of borgmatic to support converting configuration:
|
||||||
|
borgmatic 1.7.14.
|
||||||
|
|
||||||
As of version 1.1.0, borgmatic no longer supports Python 2. If you were
|
As of version 1.1.0, borgmatic no longer supports Python 2. If you were
|
||||||
already running borgmatic with Python 3, then you can upgrade borgmatic
|
already running borgmatic with Python 3, then you can upgrade borgmatic
|
||||||
in-place:
|
in-place:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo pip3 install --user --upgrade borgmatic
|
sudo pip3 install --user --upgrade borgmatic==1.7.14
|
||||||
```
|
```
|
||||||
|
|
||||||
But if you were running borgmatic with Python 2, uninstall and reinstall instead:
|
But if you were running borgmatic with Python 2, uninstall and reinstall instead:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo pip uninstall borgmatic
|
sudo pip uninstall borgmatic
|
||||||
sudo pip3 install --user borgmatic
|
sudo pip3 install --user borgmatic==1.7.14
|
||||||
```
|
```
|
||||||
|
|
||||||
The pip binary names for different versions of Python can differ, so the above
|
The pip binary names for different versions of Python can differ, so the above
|
||||||
|
@ -93,29 +94,12 @@ That will generate a new YAML configuration file at /etc/borgmatic/config.yaml
|
||||||
excludes files. The new version of borgmatic will consume the YAML
|
excludes files. The new version of borgmatic will consume the YAML
|
||||||
configuration file instead of the old one.
|
configuration file instead of the old one.
|
||||||
|
|
||||||
|
Now you can upgrade to a newer version of borgmatic:
|
||||||
### Upgrading from atticmatic
|
|
||||||
|
|
||||||
You can ignore this section if you're not an atticmatic user (the former name
|
|
||||||
of borgmatic).
|
|
||||||
|
|
||||||
borgmatic only supports Borg now and no longer supports Attic. So if you're
|
|
||||||
an Attic user, consider switching to Borg. See the [Borg upgrade
|
|
||||||
command](https://borgbackup.readthedocs.io/en/stable/usage.html#borg-upgrade)
|
|
||||||
for more information. Then, follow the instructions above about setting up
|
|
||||||
your borgmatic configuration files.
|
|
||||||
|
|
||||||
If you were already using Borg with atticmatic, then you can upgrade
|
|
||||||
from atticmatic to borgmatic by running the following commands:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo pip3 uninstall atticmatic
|
|
||||||
sudo pip3 install --user borgmatic
|
sudo pip3 install --user borgmatic
|
||||||
```
|
```
|
||||||
|
|
||||||
That's it! borgmatic will continue using your /etc/borgmatic configuration
|
|
||||||
files.
|
|
||||||
|
|
||||||
|
|
||||||
## Upgrading Borg
|
## Upgrading Borg
|
||||||
|
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -23,7 +23,6 @@ setup(
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
'borgmatic = borgmatic.commands.borgmatic:main',
|
'borgmatic = borgmatic.commands.borgmatic:main',
|
||||||
'upgrade-borgmatic-config = borgmatic.commands.convert_config:main',
|
|
||||||
'generate-borgmatic-config = borgmatic.commands.generate_config:main',
|
'generate-borgmatic-config = borgmatic.commands.generate_config:main',
|
||||||
'validate-borgmatic-config = borgmatic.commands.validate_config:main',
|
'validate-borgmatic-config = borgmatic.commands.validate_config:main',
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
import os
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from flexmock import flexmock
|
|
||||||
|
|
||||||
from borgmatic.commands import convert_config as module
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_no_arguments_uses_defaults():
|
|
||||||
flexmock(os.path).should_receive('exists').and_return(True)
|
|
||||||
|
|
||||||
parser = module.parse_arguments()
|
|
||||||
|
|
||||||
assert parser.source_config_filename == module.DEFAULT_SOURCE_CONFIG_FILENAME
|
|
||||||
assert parser.source_excludes_filename == module.DEFAULT_SOURCE_EXCLUDES_FILENAME
|
|
||||||
assert parser.destination_config_filename == module.DEFAULT_DESTINATION_CONFIG_FILENAME
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_filename_arguments_overrides_defaults():
|
|
||||||
flexmock(os.path).should_receive('exists').and_return(True)
|
|
||||||
|
|
||||||
parser = module.parse_arguments(
|
|
||||||
'--source-config',
|
|
||||||
'config',
|
|
||||||
'--source-excludes',
|
|
||||||
'excludes',
|
|
||||||
'--destination-config',
|
|
||||||
'config.yaml',
|
|
||||||
)
|
|
||||||
|
|
||||||
assert parser.source_config_filename == 'config'
|
|
||||||
assert parser.source_excludes_filename == 'excludes'
|
|
||||||
assert parser.destination_config_filename == 'config.yaml'
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_missing_default_excludes_file_sets_filename_to_none():
|
|
||||||
flexmock(os.path).should_receive('exists').and_return(False)
|
|
||||||
|
|
||||||
parser = module.parse_arguments()
|
|
||||||
|
|
||||||
assert parser.source_config_filename == module.DEFAULT_SOURCE_CONFIG_FILENAME
|
|
||||||
assert parser.source_excludes_filename is None
|
|
||||||
assert parser.destination_config_filename == module.DEFAULT_DESTINATION_CONFIG_FILENAME
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_invalid_arguments_exits():
|
|
||||||
flexmock(os.path).should_receive('exists').and_return(True)
|
|
||||||
|
|
||||||
with pytest.raises(SystemExit):
|
|
||||||
module.parse_arguments('--posix-me-harder')
|
|
|
@ -1,126 +0,0 @@
|
||||||
import os
|
|
||||||
from collections import OrderedDict, defaultdict, namedtuple
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from flexmock import flexmock
|
|
||||||
|
|
||||||
from borgmatic.config import convert as module
|
|
||||||
|
|
||||||
Parsed_config = namedtuple('Parsed_config', ('location', 'storage', 'retention', 'consistency'))
|
|
||||||
|
|
||||||
|
|
||||||
def test_convert_section_generates_integer_value_for_integer_type_in_schema():
|
|
||||||
flexmock(module.yaml.comments).should_receive('CommentedMap').replace_with(OrderedDict)
|
|
||||||
source_section_config = OrderedDict([('check_last', '3')])
|
|
||||||
section_schema = {'type': 'object', 'properties': {'check_last': {'type': 'integer'}}}
|
|
||||||
|
|
||||||
destination_config = module._convert_section(source_section_config, section_schema)
|
|
||||||
|
|
||||||
assert destination_config == OrderedDict([('check_last', 3)])
|
|
||||||
|
|
||||||
|
|
||||||
def test_convert_legacy_parsed_config_transforms_source_config_to_mapping():
|
|
||||||
flexmock(module.yaml.comments).should_receive('CommentedMap').replace_with(OrderedDict)
|
|
||||||
flexmock(module.generate).should_receive('add_comments_to_configuration_object')
|
|
||||||
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')]),
|
|
||||||
)
|
|
||||||
source_excludes = ['/var']
|
|
||||||
schema = {
|
|
||||||
'type': 'object',
|
|
||||||
'properties': defaultdict(lambda: {'type': 'object', 'properties': {}}),
|
|
||||||
}
|
|
||||||
|
|
||||||
destination_config = module.convert_legacy_parsed_config(source_config, source_excludes, schema)
|
|
||||||
|
|
||||||
assert destination_config == OrderedDict(
|
|
||||||
[
|
|
||||||
(
|
|
||||||
'location',
|
|
||||||
OrderedDict(
|
|
||||||
[
|
|
||||||
('source_directories', ['/home']),
|
|
||||||
('repositories', ['hostname.borg']),
|
|
||||||
('exclude_patterns', ['/var']),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
('storage', OrderedDict([('encryption_passphrase', 'supersecret')])),
|
|
||||||
('retention', OrderedDict([('keep_daily', 7)])),
|
|
||||||
('consistency', OrderedDict([('checks', ['repository'])])),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_convert_legacy_parsed_config_splits_space_separated_values():
|
|
||||||
flexmock(module.yaml.comments).should_receive('CommentedMap').replace_with(OrderedDict)
|
|
||||||
flexmock(module.generate).should_receive('add_comments_to_configuration_object')
|
|
||||||
source_config = Parsed_config(
|
|
||||||
location=OrderedDict(
|
|
||||||
[('source_directories', '/home /etc'), ('repository', 'hostname.borg')]
|
|
||||||
),
|
|
||||||
storage=OrderedDict(),
|
|
||||||
retention=OrderedDict(),
|
|
||||||
consistency=OrderedDict([('checks', 'repository archives')]),
|
|
||||||
)
|
|
||||||
source_excludes = ['/var']
|
|
||||||
schema = {
|
|
||||||
'type': 'object',
|
|
||||||
'properties': defaultdict(lambda: {'type': 'object', 'properties': {}}),
|
|
||||||
}
|
|
||||||
|
|
||||||
destination_config = module.convert_legacy_parsed_config(source_config, source_excludes, schema)
|
|
||||||
|
|
||||||
assert destination_config == OrderedDict(
|
|
||||||
[
|
|
||||||
(
|
|
||||||
'location',
|
|
||||||
OrderedDict(
|
|
||||||
[
|
|
||||||
('source_directories', ['/home', '/etc']),
|
|
||||||
('repositories', ['hostname.borg']),
|
|
||||||
('exclude_patterns', ['/var']),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
('storage', OrderedDict()),
|
|
||||||
('retention', OrderedDict()),
|
|
||||||
('consistency', OrderedDict([('checks', ['repository', 'archives'])])),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_guard_configuration_upgraded_raises_when_only_source_config_present():
|
|
||||||
flexmock(os.path).should_receive('exists').with_args('config').and_return(True)
|
|
||||||
flexmock(os.path).should_receive('exists').with_args('config.yaml').and_return(False)
|
|
||||||
flexmock(os.path).should_receive('exists').with_args('other.yaml').and_return(False)
|
|
||||||
|
|
||||||
with pytest.raises(module.Legacy_configuration_not_upgraded):
|
|
||||||
module.guard_configuration_upgraded('config', ('config.yaml', 'other.yaml'))
|
|
||||||
|
|
||||||
|
|
||||||
def test_guard_configuration_upgraded_does_not_raise_when_only_destination_config_present():
|
|
||||||
flexmock(os.path).should_receive('exists').with_args('config').and_return(False)
|
|
||||||
flexmock(os.path).should_receive('exists').with_args('config.yaml').and_return(False)
|
|
||||||
flexmock(os.path).should_receive('exists').with_args('other.yaml').and_return(True)
|
|
||||||
|
|
||||||
module.guard_configuration_upgraded('config', ('config.yaml', 'other.yaml'))
|
|
||||||
|
|
||||||
|
|
||||||
def test_guard_configuration_upgraded_does_not_raise_when_both_configs_present():
|
|
||||||
flexmock(os.path).should_receive('exists').with_args('config').and_return(True)
|
|
||||||
flexmock(os.path).should_receive('exists').with_args('config.yaml').and_return(False)
|
|
||||||
flexmock(os.path).should_receive('exists').with_args('other.yaml').and_return(True)
|
|
||||||
|
|
||||||
module.guard_configuration_upgraded('config', ('config.yaml', 'other.yaml'))
|
|
||||||
|
|
||||||
|
|
||||||
def test_guard_configuration_upgraded_does_not_raise_when_neither_config_present():
|
|
||||||
flexmock(os.path).should_receive('exists').with_args('config').and_return(False)
|
|
||||||
flexmock(os.path).should_receive('exists').with_args('config.yaml').and_return(False)
|
|
||||||
flexmock(os.path).should_receive('exists').with_args('other.yaml').and_return(False)
|
|
||||||
|
|
||||||
module.guard_configuration_upgraded('config', ('config.yaml', 'other.yaml'))
|
|
Loading…
Reference in a new issue