diff --git a/NEWS b/NEWS index 8f3ffc2c..099b81d2 100644 --- a/NEWS +++ b/NEWS @@ -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. diff --git a/borgmatic/commands/convert_config.py b/borgmatic/commands/convert_config.py deleted file mode 100644 index 64a89486..00000000 --- a/borgmatic/commands/convert_config.py +++ /dev/null @@ -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) diff --git a/borgmatic/config/convert.py b/borgmatic/config/convert.py deleted file mode 100644 index 093ad0c1..00000000 --- a/borgmatic/config/convert.py +++ /dev/null @@ -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() diff --git a/docs/how-to/upgrade.md b/docs/how-to/upgrade.md index 69b5f5b8..27778f13 100644 --- a/docs/how-to/upgrade.md +++ b/docs/how-to/upgrade.md @@ -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 diff --git a/setup.py b/setup.py index 8fb11b1f..4fa049e0 100644 --- a/setup.py +++ b/setup.py @@ -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', ] diff --git a/tests/integration/commands/test_convert_config.py b/tests/integration/commands/test_convert_config.py deleted file mode 100644 index db227108..00000000 --- a/tests/integration/commands/test_convert_config.py +++ /dev/null @@ -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') diff --git a/tests/unit/config/test_convert.py b/tests/unit/config/test_convert.py deleted file mode 100644 index 8ce1cdc6..00000000 --- a/tests/unit/config/test_convert.py +++ /dev/null @@ -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'))