Remove upgrade-borgmatic-config command for upgrading borgmatic 1.1.0 INI-style configuration (#529).

This commit is contained in:
Dan Helfman 2023-06-19 23:17:59 -07:00
parent 6098005f5d
commit b10aee3070
7 changed files with 7 additions and 395 deletions

2
NEWS
View file

@ -1,5 +1,7 @@
1.7.15.dev0
* #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
borgmatic has no configuration yet!
* #669: Add sample systemd user service for running borgmatic as a non-root user.

View file

@ -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)

View file

@ -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()

View file

@ -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
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
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
already running borgmatic with Python 3, then you can upgrade borgmatic
in-place:
```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:
```bash
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
@ -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
configuration file instead of the old one.
### 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:
Now you can upgrade to a newer version of borgmatic:
```bash
sudo pip3 uninstall atticmatic
sudo pip3 install --user borgmatic
```
That's it! borgmatic will continue using your /etc/borgmatic configuration
files.
## Upgrading Borg

View file

@ -23,7 +23,6 @@ setup(
entry_points={
'console_scripts': [
'borgmatic = borgmatic.commands.borgmatic:main',
'upgrade-borgmatic-config = borgmatic.commands.convert_config:main',
'generate-borgmatic-config = borgmatic.commands.generate_config:main',
'validate-borgmatic-config = borgmatic.commands.validate_config:main',
]

View file

@ -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')

View file

@ -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'))