diff --git a/NEWS b/NEWS index 341983b3..1af50d8f 100644 --- a/NEWS +++ b/NEWS @@ -7,6 +7,11 @@ * #720: Fix an error when dumping a MySQL database and the "exclude_nodump" option is set. * When merging two configuration files, error gracefully if the two files do not adhere to the same format. + * BREAKING: Remove the deprecated (and silently ignored) "--successful" flag on the "list" action, + as newer versions of Borg list successful (non-checkpoint) archives by default. + * All deprecated configuration option values now generate warning logs. + * Remove the deprecated (and non-functional) "--excludes" flag in favor of excludes within + configuration. 1.7.15 * #326: Add configuration options and command-line flags for backing up a database from one diff --git a/borgmatic/borg/list.py b/borgmatic/borg/list.py index 96a6a87f..5ec1480d 100644 --- a/borgmatic/borg/list.py +++ b/borgmatic/borg/list.py @@ -14,7 +14,6 @@ ARCHIVE_FILTER_FLAGS_MOVED_TO_RLIST = ('prefix', 'match_archives', 'sort_by', 'f MAKE_FLAGS_EXCLUDES = ( 'repository', 'archive', - 'successful', 'paths', 'find_paths', ) + ARCHIVE_FILTER_FLAGS_MOVED_TO_RLIST diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index 7e054f5a..588e31c9 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -274,11 +274,6 @@ def make_parsers(): default=config_paths, help=f"Configuration filenames or directories, defaults to: {' '.join(unexpanded_config_paths)}", ) - global_group.add_argument( - '--excludes', - dest='excludes_filename', - help='Deprecated in favor of exclude_patterns within configuration', - ) global_group.add_argument( '-n', '--dry-run', @@ -1098,12 +1093,6 @@ def make_parsers(): metavar='PATTERN', help='Only list archive names matching this pattern', ) - list_group.add_argument( - '--successful', - default=True, - action='store_true', - help='Deprecated; no effect. Newer versions of Borg shows successful (non-checkpoint) archives by default.', - ) list_group.add_argument( '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys' ) @@ -1279,11 +1268,6 @@ def parse_arguments(*unparsed_arguments): f"Unrecognized argument{'s' if len(unknown_arguments) > 1 else ''}: {' '.join(unknown_arguments)}" ) - if arguments['global'].excludes_filename: - raise ValueError( - 'The --excludes flag has been replaced with exclude_patterns in configuration.' - ) - if 'create' in arguments and arguments['create'].list_files and arguments['create'].progress: raise ValueError( 'With the create action, only one of --list (--files) and --progress flags can be used.' diff --git a/borgmatic/config/normalize.py b/borgmatic/config/normalize.py index 147e4e4e..daadfeb4 100644 --- a/borgmatic/config/normalize.py +++ b/borgmatic/config/normalize.py @@ -12,52 +12,143 @@ def normalize(config_filename, config): location = config.get('location') or {} storage = config.get('storage') or {} consistency = config.get('consistency') or {} + retention = config.get('retention') or {} hooks = config.get('hooks') or {} # Upgrade exclude_if_present from a string to a list. exclude_if_present = location.get('exclude_if_present') if isinstance(exclude_if_present, str): + logs.append( + logging.makeLogRecord( + dict( + levelno=logging.WARNING, + levelname='WARNING', + msg=f'{config_filename}: The exclude_if_present option now expects a list value. String values for this option are deprecated and support will be removed from a future release.', + ) + ) + ) config['location']['exclude_if_present'] = [exclude_if_present] # Upgrade various monitoring hooks from a string to a dict. healthchecks = hooks.get('healthchecks') if isinstance(healthchecks, str): + logs.append( + logging.makeLogRecord( + dict( + levelno=logging.WARNING, + levelname='WARNING', + msg=f'{config_filename}: The healthchecks hook now expects a mapping value. String values for this option are deprecated and support will be removed from a future release.', + ) + ) + ) config['hooks']['healthchecks'] = {'ping_url': healthchecks} cronitor = hooks.get('cronitor') if isinstance(cronitor, str): + logs.append( + logging.makeLogRecord( + dict( + levelno=logging.WARNING, + levelname='WARNING', + msg=f'{config_filename}: The healthchecks hook now expects key/value pairs. String values for this option are deprecated and support will be removed from a future release.', + ) + ) + ) config['hooks']['cronitor'] = {'ping_url': cronitor} pagerduty = hooks.get('pagerduty') if isinstance(pagerduty, str): + logs.append( + logging.makeLogRecord( + dict( + levelno=logging.WARNING, + levelname='WARNING', + msg=f'{config_filename}: The healthchecks hook now expects key/value pairs. String values for this option are deprecated and support will be removed from a future release.', + ) + ) + ) config['hooks']['pagerduty'] = {'integration_key': pagerduty} cronhub = hooks.get('cronhub') if isinstance(cronhub, str): + logs.append( + logging.makeLogRecord( + dict( + levelno=logging.WARNING, + levelname='WARNING', + msg=f'{config_filename}: The healthchecks hook now expects key/value pairs. String values for this option are deprecated and support will be removed from a future release.', + ) + ) + ) config['hooks']['cronhub'] = {'ping_url': cronhub} # Upgrade consistency checks from a list of strings to a list of dicts. checks = consistency.get('checks') if isinstance(checks, list) and len(checks) and isinstance(checks[0], str): + logs.append( + logging.makeLogRecord( + dict( + levelno=logging.WARNING, + levelname='WARNING', + msg=f'{config_filename}: The checks option now expects a list of key/value pairs. Lists of strings for this option are deprecated and support will be removed from a future release.', + ) + ) + ) config['consistency']['checks'] = [{'name': check_type} for check_type in checks] # Rename various configuration options. numeric_owner = location.pop('numeric_owner', None) if numeric_owner is not None: + logs.append( + logging.makeLogRecord( + dict( + levelno=logging.WARNING, + levelname='WARNING', + msg=f'{config_filename}: The numeric_owner option has been renamed to numeric_ids. numeric_owner is deprecated and support will be removed from a future release.', + ) + ) + ) config['location']['numeric_ids'] = numeric_owner bsd_flags = location.pop('bsd_flags', None) if bsd_flags is not None: + logs.append( + logging.makeLogRecord( + dict( + levelno=logging.WARNING, + levelname='WARNING', + msg=f'{config_filename}: The bsd_flags option has been renamed to flags. bsd_flags is deprecated and support will be removed from a future release.', + ) + ) + ) config['location']['flags'] = bsd_flags remote_rate_limit = storage.pop('remote_rate_limit', None) if remote_rate_limit is not None: + logs.append( + logging.makeLogRecord( + dict( + levelno=logging.WARNING, + levelname='WARNING', + msg=f'{config_filename}: The remote_rate_limit option has been renamed to upload_rate_limit. remote_rate_limit is deprecated and support will be removed from a future release.', + ) + ) + ) config['storage']['upload_rate_limit'] = remote_rate_limit # Upgrade remote repositories to ssh:// syntax, required in Borg 2. repositories = location.get('repositories') if repositories: if isinstance(repositories[0], str): + logs.append( + logging.makeLogRecord( + dict( + levelno=logging.WARNING, + levelname='WARNING', + msg=f'{config_filename}: The repositories option now expects a list of key/value pairs. Lists of strings for this option are deprecated and support will be removed from a future release.', + ) + ) + ) config['location']['repositories'] = [ {'path': repository} for repository in repositories ] @@ -71,7 +162,7 @@ def normalize(config_filename, config): dict( levelno=logging.WARNING, levelname='WARNING', - msg=f'{config_filename}: Repository paths containing "~" are deprecated in borgmatic and no longer work in Borg 2.x+.', + msg=f'{config_filename}: Repository paths containing "~" are deprecated in borgmatic and support will be removed from a future release.', ) ) ) @@ -95,7 +186,7 @@ def normalize(config_filename, config): dict( levelno=logging.WARNING, levelname='WARNING', - msg=f'{config_filename}: Remote repository paths without ssh:// syntax are deprecated. Interpreting "{repository_path}" as "{rewritten_repository_path}"', + msg=f'{config_filename}: Remote repository paths without ssh:// syntax are deprecated and support will be removed from a future release. Interpreting "{repository_path}" as "{rewritten_repository_path}"', ) ) ) @@ -108,4 +199,15 @@ def normalize(config_filename, config): else: config['location']['repositories'].append(repository_dict) + if consistency.get('prefix') or retention.get('prefix'): + logs.append( + logging.makeLogRecord( + dict( + levelno=logging.WARNING, + levelname='WARNING', + msg=f'{config_filename}: The prefix option is deprecated and support will be removed from a future release. Use archive_name_format or match_archives instead.', + ) + ) + ) + return logs diff --git a/tests/integration/commands/test_arguments.py b/tests/integration/commands/test_arguments.py index 27dc3234..db8db636 100644 --- a/tests/integration/commands/test_arguments.py +++ b/tests/integration/commands/test_arguments.py @@ -12,7 +12,6 @@ def test_parse_arguments_with_no_arguments_uses_defaults(): global_arguments = arguments['global'] assert global_arguments.config_paths == config_paths - assert global_arguments.excludes_filename is None assert global_arguments.verbosity == 0 assert global_arguments.syslog_verbosity == 0 assert global_arguments.log_file_verbosity == 0 @@ -71,7 +70,6 @@ def test_parse_arguments_with_verbosity_overrides_default(): global_arguments = arguments['global'] assert global_arguments.config_paths == config_paths - assert global_arguments.excludes_filename is None assert global_arguments.verbosity == 1 assert global_arguments.syslog_verbosity == 0 assert global_arguments.log_file_verbosity == 0 @@ -85,7 +83,6 @@ def test_parse_arguments_with_syslog_verbosity_overrides_default(): global_arguments = arguments['global'] assert global_arguments.config_paths == config_paths - assert global_arguments.excludes_filename is None assert global_arguments.verbosity == 0 assert global_arguments.syslog_verbosity == 2 @@ -98,7 +95,6 @@ def test_parse_arguments_with_log_file_verbosity_overrides_default(): global_arguments = arguments['global'] assert global_arguments.config_paths == config_paths - assert global_arguments.excludes_filename is None assert global_arguments.verbosity == 0 assert global_arguments.syslog_verbosity == 0 assert global_arguments.log_file_verbosity == -1 @@ -234,13 +230,6 @@ def test_parse_arguments_disallows_invalid_argument(): module.parse_arguments('--posix-me-harder') -def test_parse_arguments_disallows_deprecated_excludes_option(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - with pytest.raises(ValueError): - module.parse_arguments('--config', 'myconfig', '--excludes', 'myexcludes') - - def test_parse_arguments_disallows_encryption_mode_without_init(): flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) diff --git a/tests/integration/config/test_validate.py b/tests/integration/config/test_validate.py index 87428ddc..1abc3ba5 100644 --- a/tests/integration/config/test_validate.py +++ b/tests/integration/config/test_validate.py @@ -46,7 +46,7 @@ def test_parse_configuration_transforms_file_into_mapping(): - /etc repositories: - - hostname.borg + - path: hostname.borg retention: keep_minutely: 60 @@ -83,7 +83,7 @@ def test_parse_configuration_passes_through_quoted_punctuation(): - "/home/{escaped_punctuation}" repositories: - - test.borg + - path: test.borg ''' ) @@ -106,7 +106,7 @@ def test_parse_configuration_with_schema_lacking_examples_does_not_raise(): - /home repositories: - - hostname.borg + - path: hostname.borg ''', ''' map: @@ -135,7 +135,7 @@ def test_parse_configuration_inlines_include(): - /home repositories: - - hostname.borg + - path: hostname.borg retention: !include include.yaml @@ -168,7 +168,7 @@ def test_parse_configuration_merges_include(): - /home repositories: - - hostname.borg + - path: hostname.borg retention: keep_daily: 1 @@ -221,7 +221,7 @@ def test_parse_configuration_raises_for_validation_error(): location: source_directories: yes repositories: - - hostname.borg + - path: hostname.borg ''' ) @@ -237,7 +237,7 @@ def test_parse_configuration_applies_overrides(): - /home repositories: - - hostname.borg + - path: hostname.borg local_path: borg1 ''' @@ -265,7 +265,7 @@ def test_parse_configuration_applies_normalization(): - /home repositories: - - hostname.borg + - path: hostname.borg exclude_if_present: .nobackup ''' @@ -280,4 +280,4 @@ def test_parse_configuration_applies_normalization(): 'exclude_if_present': ['.nobackup'], } } - assert logs == [] + assert logs diff --git a/tests/unit/config/test_normalize.py b/tests/unit/config/test_normalize.py index 63e3187a..6393d902 100644 --- a/tests/unit/config/test_normalize.py +++ b/tests/unit/config/test_normalize.py @@ -9,7 +9,7 @@ from borgmatic.config import normalize as module ( {'location': {'exclude_if_present': '.nobackup'}}, {'location': {'exclude_if_present': ['.nobackup']}}, - False, + True, ), ( {'location': {'exclude_if_present': ['.nobackup']}}, @@ -39,22 +39,22 @@ from borgmatic.config import normalize as module ( {'hooks': {'healthchecks': 'https://example.com'}}, {'hooks': {'healthchecks': {'ping_url': 'https://example.com'}}}, - False, + True, ), ( {'hooks': {'cronitor': 'https://example.com'}}, {'hooks': {'cronitor': {'ping_url': 'https://example.com'}}}, - False, + True, ), ( {'hooks': {'pagerduty': 'https://example.com'}}, {'hooks': {'pagerduty': {'integration_key': 'https://example.com'}}}, - False, + True, ), ( {'hooks': {'cronhub': 'https://example.com'}}, {'hooks': {'cronhub': {'ping_url': 'https://example.com'}}}, - False, + True, ), ( {'hooks': None}, @@ -64,12 +64,12 @@ from borgmatic.config import normalize as module ( {'consistency': {'checks': ['archives']}}, {'consistency': {'checks': [{'name': 'archives'}]}}, - False, + True, ), ( {'consistency': {'checks': ['archives']}}, {'consistency': {'checks': [{'name': 'archives'}]}}, - False, + True, ), ( {'consistency': None}, @@ -79,17 +79,17 @@ from borgmatic.config import normalize as module ( {'location': {'numeric_owner': False}}, {'location': {'numeric_ids': False}}, - False, + True, ), ( {'location': {'bsd_flags': False}}, {'location': {'flags': False}}, - False, + True, ), ( {'storage': {'remote_rate_limit': False}}, {'storage': {'upload_rate_limit': False}}, - False, + True, ), ( {'location': {'repositories': ['foo@bar:/repo']}}, @@ -109,12 +109,12 @@ from borgmatic.config import normalize as module ( {'location': {'repositories': ['ssh://foo@bar:1234/repo']}}, {'location': {'repositories': [{'path': 'ssh://foo@bar:1234/repo'}]}}, - False, + True, ), ( {'location': {'repositories': ['file:///repo']}}, {'location': {'repositories': [{'path': '/repo'}]}}, - False, + True, ), ( {'location': {'repositories': [{'path': 'foo@bar:/repo', 'label': 'foo'}]}}, @@ -131,6 +131,16 @@ from borgmatic.config import normalize as module {'location': {'repositories': [{'path': '/repo', 'label': 'foo'}]}}, False, ), + ( + {'consistency': {'prefix': 'foo'}}, + {'consistency': {'prefix': 'foo'}}, + True, + ), + ( + {'retention': {'prefix': 'foo'}}, + {'retention': {'prefix': 'foo'}}, + True, + ), ), ) def test_normalize_applies_hard_coded_normalization_to_config(