From 16066942e367bfbfeb32f891fcdad19358d41aa5 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Tue, 19 Jul 2022 10:25:10 -0700 Subject: [PATCH 01/38] Fix traceback with "create" action and "--json" flag when a database hook is configured (#563). --- NEWS | 1 + borgmatic/execute.py | 34 +++++++++++++++++++++++-------- tests/integration/test_execute.py | 24 ++++++++++++++++++++++ tests/unit/test_execute.py | 21 +++++++++++++++++++ 4 files changed, 71 insertions(+), 9 deletions(-) diff --git a/NEWS b/NEWS index 7edc60e4b..d8b12db98 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,7 @@ * #560: Fix all database hooks to error when the requested database to restore isn't present in the Borg archive. * #561: Fix command-line "--override" flag to continue supporting old configuration file formats. + * #563: Fix traceback with "create" action and "--json" flag when a database hook is configured. 1.6.5 * #553: Fix logging to include the full traceback when Borg experiences an internal error, not just diff --git a/borgmatic/execute.py b/borgmatic/execute.py index 8760703ed..a9874bf32 100644 --- a/borgmatic/execute.py +++ b/borgmatic/execute.py @@ -51,6 +51,9 @@ def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path): process with the requested log level. Additionally, raise a CalledProcessError if a process exits with an error (or a warning for exit code 1, if that process matches the Borg local path). + If output log level is None, then instead of logging, capture output for each process and return + it as a dict from the process to its output. + For simplicity, it's assumed that the output buffer for each process is its stdout. But if any stdouts are given to exclude, then for any matching processes, log from their stderr instead. @@ -65,6 +68,7 @@ def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path): if process.stdout or process.stderr } output_buffers = list(process_for_output_buffer.keys()) + captured_outputs = collections.defaultdict(list) # Log output for each process until they all exit. while True: @@ -99,7 +103,10 @@ def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path): if len(last_lines) > ERROR_OUTPUT_MAX_LINE_COUNT: last_lines.pop(0) - logger.log(output_log_level, line) + if output_log_level is None: + captured_outputs[ready_process].append(line) + else: + logger.log(output_log_level, line) still_running = False @@ -133,6 +140,11 @@ def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path): if not still_running: break + if captured_outputs: + return { + process: '\n'.join(output_lines) for process, output_lines in captured_outputs.items() + } + def log_command(full_command, input_file, output_file): ''' @@ -222,13 +234,14 @@ def execute_command_with_processes( run as well. This is useful, for instance, for processes that are streaming output to a named pipe that the given command is consuming from. - If an open output file object is given, then write stdout to the file and only log stderr (but - only if an output log level is set). If an open input file object is given, then read stdin from - the file. If shell is True, execute the command within a shell. If an extra environment dict is - given, then use it to augment the current environment, and pass the result into the command. If - a working directory is given, use that as the present working directory when running the - command. If a Borg local path is given, then for any matching command or process (regardless of - arguments), treat exit code 1 as a warning instead of an error. + If an open output file object is given, then write stdout to the file and only log stderr. But + if output log level is None, instead suppress logging and return the captured output for (only) + the given command. If an open input file object is given, then read stdin from the file. If + shell is True, execute the command within a shell. If an extra environment dict is given, then + use it to augment the current environment, and pass the result into the command. If a working + directory is given, use that as the present working directory when running the command. If a + Borg local path is given, then for any matching command or process (regardless of arguments), + treat exit code 1 as a warning instead of an error. Raise subprocesses.CalledProcessError if an error occurs while running the command or in the upstream process. @@ -259,9 +272,12 @@ def execute_command_with_processes( process.kill() raise - log_outputs( + captured_outputs = log_outputs( tuple(processes) + (command_process,), (input_file, output_file), output_log_level, borg_local_path=borg_local_path, ) + + if output_log_level is None: + return captured_outputs.get(command_process) diff --git a/tests/integration/test_execute.py b/tests/integration/test_execute.py index 1647a9e09..bb618eccc 100644 --- a/tests/integration/test_execute.py +++ b/tests/integration/test_execute.py @@ -54,6 +54,30 @@ def test_log_outputs_skips_logs_for_process_with_none_stdout(): ) +def test_log_outputs_returns_output_without_logging_for_output_log_level_none(): + flexmock(module.logger).should_receive('log').never() + flexmock(module).should_receive('exit_code_indicates_error').and_return(False) + + hi_process = subprocess.Popen(['echo', 'hi'], stdout=subprocess.PIPE) + flexmock(module).should_receive('output_buffer_for_process').with_args( + hi_process, () + ).and_return(hi_process.stdout) + + there_process = subprocess.Popen(['echo', 'there'], stdout=subprocess.PIPE) + flexmock(module).should_receive('output_buffer_for_process').with_args( + there_process, () + ).and_return(there_process.stdout) + + captured_outputs = module.log_outputs( + (hi_process, there_process), + exclude_stdouts=(), + output_log_level=None, + borg_local_path='borg', + ) + + assert captured_outputs == {hi_process: 'hi', there_process: 'there'} + + def test_log_outputs_includes_error_output_in_exception(): flexmock(module.logger).should_receive('log') flexmock(module).should_receive('exit_code_indicates_error').and_return(True) diff --git a/tests/unit/test_execute.py b/tests/unit/test_execute.py index c747a9497..ab4f15ad5 100644 --- a/tests/unit/test_execute.py +++ b/tests/unit/test_execute.py @@ -289,6 +289,27 @@ def test_execute_command_with_processes_calls_full_command(): assert output is None +def test_execute_command_with_processes_returns_output_with_output_log_level_none(): + full_command = ['foo', 'bar'] + processes = (flexmock(),) + flexmock(module.os, environ={'a': 'b'}) + process = flexmock(stdout=None) + flexmock(module.subprocess).should_receive('Popen').with_args( + full_command, + stdin=None, + stdout=module.subprocess.PIPE, + stderr=module.subprocess.STDOUT, + shell=False, + env=None, + cwd=None, + ).and_return(process).once() + flexmock(module).should_receive('log_outputs').and_return({process: 'out'}) + + output = module.execute_command_with_processes(full_command, processes, output_log_level=None) + + assert output == 'out' + + def test_execute_command_with_processes_calls_full_command_with_output_file(): full_command = ['foo', 'bar'] processes = (flexmock(),) From 23feac2f4c337a0a06805e4e23186905564b889f Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Tue, 19 Jul 2022 20:32:41 -0700 Subject: [PATCH 02/38] Bump version for release. --- NEWS | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/NEWS b/NEWS index d8b12db98..0b37437b3 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,4 @@ -1.6.6.dev0 +1.6.6 * #559: Update documentation about configuring multiple consistency checks or multiple databases. * #560: Fix all database hooks to error when the requested database to restore isn't present in the Borg archive. diff --git a/setup.py b/setup.py index 9cf64a16b..e084f4438 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import find_packages, setup -VERSION = '1.6.6.dev0' +VERSION = '1.6.6' setup( From 6ddae20fa1f2f8c8207d18f74595c96dc6f55569 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Sat, 23 Jul 2022 21:02:21 -0700 Subject: [PATCH 03/38] Fix handling of "repository" and "data" consistency checks to prevent invalid Borg flags (#565). --- NEWS | 3 +++ borgmatic/borg/check.py | 15 ++++++++------- setup.py | 2 +- tests/unit/borg/test_check.py | 28 ++++++++-------------------- 4 files changed, 20 insertions(+), 28 deletions(-) diff --git a/NEWS b/NEWS index 0b37437b3..55c4fca7a 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,6 @@ +1.6.7.dev0 + * #565: Fix handling of "repository" and "data" consistency checks to prevent invalid Borg flags. + 1.6.6 * #559: Update documentation about configuring multiple consistency checks or multiple databases. * #560: Fix all database hooks to error when the requested database to restore isn't present in the diff --git a/borgmatic/borg/check.py b/borgmatic/borg/check.py index 83c3eea2d..665256fdc 100644 --- a/borgmatic/borg/check.py +++ b/borgmatic/borg/check.py @@ -33,8 +33,6 @@ def parse_checks(consistency_config, only_checks=None): If no "checks" option is present in the config, return the DEFAULT_CHECKS. If a checks value has a name of "disabled", return an empty tuple, meaning that no checks should be run. - - If the "data" check is present, then make sure the "archives" check is included as well. ''' checks = only_checks or tuple( check_config['name'] @@ -48,9 +46,6 @@ def parse_checks(consistency_config, only_checks=None): ) return () - if 'data' in checks and 'archives' not in checks: - return checks + ('archives',) - return checks @@ -164,7 +159,7 @@ def make_check_flags(checks, check_last=None, prefix=None): ('--repository-only',) However, if both "repository" and "archives" are in checks, then omit them from the returned - flags because Borg does both checks by default. + flags because Borg does both checks by default. If "data" is in checks, that implies "archives". Additionally, if a check_last value is given and "archives" is in checks, then include a "--last" flag. And if a prefix value is given and "archives" is in checks, then include a @@ -183,7 +178,13 @@ def make_check_flags(checks, check_last=None, prefix=None): 'Ignoring consistency prefix option, as "archives" is not in consistency checks' ) - common_flags = last_flags + prefix_flags + (('--verify-data',) if 'data' in checks else ()) + if 'data' in checks: + data_flags = ('--verify-data',) + checks += ('archives',) + else: + data_flags = () + + common_flags = last_flags + prefix_flags + data_flags if {'repository', 'archives'}.issubset(set(checks)): return common_flags diff --git a/setup.py b/setup.py index e084f4438..4b2c6ee9a 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import find_packages, setup -VERSION = '1.6.6' +VERSION = '1.6.7.dev0' setup( diff --git a/tests/unit/borg/test_check.py b/tests/unit/borg/test_check.py index 0b4c0cef5..94a913369 100644 --- a/tests/unit/borg/test_check.py +++ b/tests/unit/borg/test_check.py @@ -49,18 +49,6 @@ def test_parse_checks_with_disabled_returns_no_checks(): assert checks == () -def test_parse_checks_with_data_check_also_injects_archives(): - checks = module.parse_checks({'checks': [{'name': 'data'}]}) - - assert checks == ('data', 'archives') - - -def test_parse_checks_with_data_check_passes_through_archives(): - checks = module.parse_checks({'checks': [{'name': 'data'}, {'name': 'archives'}]}) - - assert checks == ('data', 'archives') - - def test_parse_checks_prefers_override_checks_to_configured_checks(): checks = module.parse_checks( {'checks': [{'name': 'archives'}]}, only_checks=['repository', 'extract'] @@ -69,12 +57,6 @@ def test_parse_checks_prefers_override_checks_to_configured_checks(): assert checks == ('repository', 'extract') -def test_parse_checks_with_override_data_check_also_injects_archives(): - checks = module.parse_checks({'checks': [{'name': 'extract'}]}, only_checks=['data']) - - assert checks == ('data', 'archives') - - @pytest.mark.parametrize( 'frequency,expected_result', ( @@ -217,10 +199,10 @@ def test_make_check_flags_with_archives_check_returns_flag(): assert flags == ('--archives-only',) -def test_make_check_flags_with_data_check_returns_flag(): +def test_make_check_flags_with_data_check_returns_flag_and_implies_archives(): flags = module.make_check_flags(('data',)) - assert flags == ('--verify-data',) + assert flags == ('--archives-only', '--verify-data',) def test_make_check_flags_with_extract_omits_extract_flag(): @@ -229,6 +211,12 @@ def test_make_check_flags_with_extract_omits_extract_flag(): assert flags == () +def test_make_check_flags_with_repository_and_data_checks_does_not_return_repository_only(): + flags = module.make_check_flags(('repository', 'data',)) + + assert flags == ('--verify-data',) + + def test_make_check_flags_with_default_checks_and_default_prefix_returns_default_flags(): flags = module.make_check_flags(('repository', 'archives'), prefix=module.DEFAULT_PREFIX) From 5c6407047ff7d07b4d05b4cd5bff587ab4e4b8a2 Mon Sep 17 00:00:00 2001 From: Uli Date: Sat, 23 Jul 2022 22:07:06 +0200 Subject: [PATCH 04/38] feat: add verify_tls flag for Healthchecks --- borgmatic/config/schema.yaml | 6 ++++ borgmatic/hooks/healthchecks.py | 4 ++- tests/unit/hooks/test_healthchecks.py | 50 +++++++++++++++++++++++---- 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index 02caf7929..1880778c9 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -1012,6 +1012,12 @@ properties: Healthchecks ping URL or UUID to notify when a backup begins, ends, or errors. example: https://hc-ping.com/your-uuid-here + verify_tls: + type: boolean + description: | + Verify the SSL certificate of the endpoint. + Defaults to true. + example: false send_logs: type: boolean description: | diff --git a/borgmatic/hooks/healthchecks.py b/borgmatic/hooks/healthchecks.py index c801f1824..03d012a8f 100644 --- a/borgmatic/hooks/healthchecks.py +++ b/borgmatic/hooks/healthchecks.py @@ -125,7 +125,9 @@ def ping_monitor(hook_config, config_filename, state, monitoring_log_level, dry_ if not dry_run: logging.getLogger('urllib3').setLevel(logging.ERROR) try: - response = requests.post(ping_url, data=payload.encode('utf-8')) + response = requests.post( + ping_url, data=payload.encode('utf-8'), verify=hook_config.get('verify_tls', True) + ) if not response.ok: response.raise_for_status() except requests.exceptions.RequestException as error: diff --git a/tests/unit/hooks/test_healthchecks.py b/tests/unit/hooks/test_healthchecks.py index 65c5613d3..ee78e52b3 100644 --- a/tests/unit/hooks/test_healthchecks.py +++ b/tests/unit/hooks/test_healthchecks.py @@ -138,7 +138,7 @@ def test_ping_monitor_hits_ping_url_for_start_state(): flexmock(module).should_receive('Forgetful_buffering_handler') hook_config = {'ping_url': 'https://example.com'} flexmock(module.requests).should_receive('post').with_args( - 'https://example.com/start', data=''.encode('utf-8') + 'https://example.com/start', data=''.encode('utf-8'), verify=True ).and_return(flexmock(ok=True)) module.ping_monitor( @@ -155,7 +155,7 @@ def test_ping_monitor_hits_ping_url_for_finish_state(): payload = 'data' flexmock(module).should_receive('format_buffered_logs_for_payload').and_return(payload) flexmock(module.requests).should_receive('post').with_args( - 'https://example.com', data=payload.encode('utf-8') + 'https://example.com', data=payload.encode('utf-8'), verify=True ).and_return(flexmock(ok=True)) module.ping_monitor( @@ -172,7 +172,7 @@ def test_ping_monitor_hits_ping_url_for_fail_state(): payload = 'data' flexmock(module).should_receive('format_buffered_logs_for_payload').and_return(payload) flexmock(module.requests).should_receive('post').with_args( - 'https://example.com/fail', data=payload.encode('utf') + 'https://example.com/fail', data=payload.encode('utf'), verify=True ).and_return(flexmock(ok=True)) module.ping_monitor( @@ -189,7 +189,43 @@ def test_ping_monitor_with_ping_uuid_hits_corresponding_url(): payload = 'data' flexmock(module).should_receive('format_buffered_logs_for_payload').and_return(payload) flexmock(module.requests).should_receive('post').with_args( - 'https://hc-ping.com/{}'.format(hook_config['ping_url']), data=payload.encode('utf-8') + 'https://hc-ping.com/{}'.format(hook_config['ping_url']), + data=payload.encode('utf-8'), + verify=True, + ).and_return(flexmock(ok=True)) + + module.ping_monitor( + hook_config, + 'config.yaml', + state=module.monitor.State.FINISH, + monitoring_log_level=1, + dry_run=False, + ) + + +def test_ping_monitor_skips_ssl_verification_when_verify_tls_false(): + hook_config = {'ping_url': 'https://example.com', 'verify_tls': False} + payload = 'data' + flexmock(module).should_receive('format_buffered_logs_for_payload').and_return(payload) + flexmock(module.requests).should_receive('post').with_args( + 'https://example.com', data=payload.encode('utf-8'), verify=False + ).and_return(flexmock(ok=True)) + + module.ping_monitor( + hook_config, + 'config.yaml', + state=module.monitor.State.FINISH, + monitoring_log_level=1, + dry_run=False, + ) + + +def test_ping_monitor_executes_ssl_verification_when_verify_tls_true(): + hook_config = {'ping_url': 'https://example.com', 'verify_tls': True} + payload = 'data' + flexmock(module).should_receive('format_buffered_logs_for_payload').and_return(payload) + flexmock(module.requests).should_receive('post').with_args( + 'https://example.com', data=payload.encode('utf-8'), verify=True ).and_return(flexmock(ok=True)) module.ping_monitor( @@ -233,7 +269,7 @@ def test_ping_monitor_hits_ping_url_when_states_matching(): flexmock(module).should_receive('Forgetful_buffering_handler') hook_config = {'ping_url': 'https://example.com', 'states': ['start', 'finish']} flexmock(module.requests).should_receive('post').with_args( - 'https://example.com/start', data=''.encode('utf-8') + 'https://example.com/start', data=''.encode('utf-8'), verify=True ).and_return(flexmock(ok=True)) module.ping_monitor( @@ -249,7 +285,7 @@ def test_ping_monitor_with_connection_error_logs_warning(): flexmock(module).should_receive('Forgetful_buffering_handler') hook_config = {'ping_url': 'https://example.com'} flexmock(module.requests).should_receive('post').with_args( - 'https://example.com/start', data=''.encode('utf-8') + 'https://example.com/start', data=''.encode('utf-8'), verify=True ).and_raise(module.requests.exceptions.ConnectionError) flexmock(module.logger).should_receive('warning').once() @@ -270,7 +306,7 @@ def test_ping_monitor_with_other_error_logs_warning(): module.requests.exceptions.RequestException ) flexmock(module.requests).should_receive('post').with_args( - 'https://example.com/start', data=''.encode('utf-8') + 'https://example.com/start', data=''.encode('utf-8'), verify=True ).and_return(response) flexmock(module.logger).should_receive('warning').once() From c7e4e6f6c995dfc4c0e0956f0dcf0463c40f8cda Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Sat, 23 Jul 2022 23:16:06 -0700 Subject: [PATCH 05/38] Add Healthchecks "verify_tls" option to NEWS. --- NEWS | 2 ++ borgmatic/config/schema.yaml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 55c4fca7a..e18e13010 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,7 @@ 1.6.7.dev0 * #565: Fix handling of "repository" and "data" consistency checks to prevent invalid Borg flags. + * Add support for disabling TLS verification in Healthchecks monitoring hook with "verify_tls" + option. 1.6.6 * #559: Update documentation about configuring multiple consistency checks or multiple databases. diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index 1880778c9..83c163d4f 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -1015,7 +1015,7 @@ properties: verify_tls: type: boolean description: | - Verify the SSL certificate of the endpoint. + Verify the TLS certificate of the ping URL host. Defaults to true. example: false send_logs: From 9aece3936a7828166479145c0d31455014ff4882 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Mon, 25 Jul 2022 11:30:02 -0700 Subject: [PATCH 06/38] Modify "mount" and "extract" actions to require the "--repository" flag when multiple repositories are configured (#566). --- NEWS | 2 ++ borgmatic/commands/borgmatic.py | 28 +++++++++--------- borgmatic/config/validate.py | 41 ++++++++++++++++----------- tests/unit/commands/test_borgmatic.py | 7 +++++ tests/unit/config/test_validate.py | 35 +++++++++++++++++------ 5 files changed, 75 insertions(+), 38 deletions(-) diff --git a/NEWS b/NEWS index e18e13010..f7002a315 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,7 @@ 1.6.7.dev0 * #565: Fix handling of "repository" and "data" consistency checks to prevent invalid Borg flags. + * #566: Modify "mount" and "extract" actions to require the "--repository" flag when multiple + repositories are configured. * Add support for disabling TLS verification in Healthchecks monitoring hook with "verify_tls" option. diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index 1ec250ffe..2e11c97d6 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -768,21 +768,21 @@ def collect_configuration_run_summary_logs(configs, arguments): any, to stdout. ''' # Run cross-file validation checks. - if 'extract' in arguments: - repository = arguments['extract'].repository - elif 'list' in arguments and arguments['list'].archive: - repository = arguments['list'].repository - elif 'mount' in arguments: - repository = arguments['mount'].repository - else: - repository = None + repository = None - if repository: - try: - validate.guard_configuration_contains_repository(repository, configs) - except ValueError as error: - yield from log_error_records(str(error)) - return + for action_name, action_arguments in arguments.items(): + if hasattr(action_arguments, 'repository'): + repository = getattr(action_arguments, 'repository') + break + + try: + if 'extract' in arguments or 'mount' in arguments: + validate.guard_single_repository_selected(repository, configs) + + validate.guard_configuration_contains_repository(repository, configs) + except ValueError as error: + yield from log_error_records(str(error)) + return if not configs: yield from log_error_records( diff --git a/borgmatic/config/validate.py b/borgmatic/config/validate.py index a782d7bbe..b0e89cd9e 100644 --- a/borgmatic/config/validate.py +++ b/borgmatic/config/validate.py @@ -140,27 +140,13 @@ def repositories_match(first, second): def guard_configuration_contains_repository(repository, configurations): ''' Given a repository path and a dict mapping from config filename to corresponding parsed config - dict, ensure that the repository is declared exactly once in all of the configurations. - - If no repository is given, then error if there are multiple configured repositories. + dict, ensure that the repository is declared exactly once in all of the configurations. If no + repository is given, skip this check. Raise ValueError if the repository is not found in a configuration, or is declared multiple times. ''' if not repository: - count = len( - tuple( - config_repository - for config in configurations.values() - for config_repository in config['location']['repositories'] - ) - ) - - if count > 1: - raise ValueError( - 'Can\'t determine which repository to use. Use --repository option to disambiguate' - ) - return count = len( @@ -176,3 +162,26 @@ def guard_configuration_contains_repository(repository, configurations): raise ValueError('Repository {} not found in configuration files'.format(repository)) if count > 1: raise ValueError('Repository {} found in multiple configuration files'.format(repository)) + + +def guard_single_repository_selected(repository, configurations): + ''' + Given a repository path and a dict mapping from config filename to corresponding parsed config + dict, ensure either a single repository exists across all configuration files or a repository + path was given. + ''' + if repository: + return + + count = len( + tuple( + config_repository + for config in configurations.values() + for config_repository in config['location']['repositories'] + ) + ) + + if count != 1: + raise ValueError( + 'Can\'t determine which repository to use. Use --repository to disambiguate' + ) diff --git a/tests/unit/commands/test_borgmatic.py b/tests/unit/commands/test_borgmatic.py index bc62d7a78..66c5abefd 100644 --- a/tests/unit/commands/test_borgmatic.py +++ b/tests/unit/commands/test_borgmatic.py @@ -746,6 +746,7 @@ def test_get_local_path_without_local_path_defaults_to_borg(): def test_collect_configuration_run_summary_logs_info_for_success(): flexmock(module.command).should_receive('execute_hook').never() + flexmock(module.validate).should_receive('guard_configuration_contains_repository') flexmock(module).should_receive('run_configuration').and_return([]) arguments = {} @@ -757,6 +758,7 @@ def test_collect_configuration_run_summary_logs_info_for_success(): def test_collect_configuration_run_summary_executes_hooks_for_create(): + flexmock(module.validate).should_receive('guard_configuration_contains_repository') flexmock(module).should_receive('run_configuration').and_return([]) arguments = {'create': flexmock(), 'global': flexmock(monitoring_verbosity=1, dry_run=False)} @@ -768,6 +770,7 @@ def test_collect_configuration_run_summary_executes_hooks_for_create(): def test_collect_configuration_run_summary_logs_info_for_success_with_extract(): + flexmock(module.validate).should_receive('guard_single_repository_selected') flexmock(module.validate).should_receive('guard_configuration_contains_repository') flexmock(module).should_receive('run_configuration').and_return([]) arguments = {'extract': flexmock(repository='repo')} @@ -795,6 +798,7 @@ def test_collect_configuration_run_summary_logs_extract_with_repository_error(): def test_collect_configuration_run_summary_logs_info_for_success_with_mount(): + flexmock(module.validate).should_receive('guard_single_repository_selected') flexmock(module.validate).should_receive('guard_configuration_contains_repository') flexmock(module).should_receive('run_configuration').and_return([]) arguments = {'mount': flexmock(repository='repo')} @@ -846,6 +850,7 @@ def test_collect_configuration_run_summary_logs_pre_hook_error(): def test_collect_configuration_run_summary_logs_post_hook_error(): flexmock(module.command).should_receive('execute_hook').and_return(None).and_raise(ValueError) + flexmock(module.validate).should_receive('guard_configuration_contains_repository') flexmock(module).should_receive('run_configuration').and_return([]) expected_logs = (flexmock(),) flexmock(module).should_receive('log_error_records').and_return(expected_logs) @@ -874,6 +879,7 @@ def test_collect_configuration_run_summary_logs_for_list_with_archive_and_reposi def test_collect_configuration_run_summary_logs_info_for_success_with_list(): + flexmock(module.validate).should_receive('guard_configuration_contains_repository') flexmock(module).should_receive('run_configuration').and_return([]) arguments = {'list': flexmock(repository='repo', archive=None)} @@ -916,6 +922,7 @@ def test_collect_configuration_run_summary_logs_run_umount_error(): def test_collect_configuration_run_summary_logs_outputs_merged_json_results(): + flexmock(module.validate).should_receive('guard_configuration_contains_repository') flexmock(module).should_receive('run_configuration').and_return(['foo', 'bar']).and_return( ['baz'] ) diff --git a/tests/unit/config/test_validate.py b/tests/unit/config/test_validate.py index a8588992a..713ecc7a2 100644 --- a/tests/unit/config/test_validate.py +++ b/tests/unit/config/test_validate.py @@ -120,14 +120,6 @@ def test_guard_configuration_contains_repository_does_not_raise_when_repository_ ) -def test_guard_configuration_contains_repository_errors_when_repository_assumed_to_match_config_twice(): - with pytest.raises(ValueError): - module.guard_configuration_contains_repository( - repository=None, - configurations={'config.yaml': {'location': {'repositories': ['repo', 'repo2']}}}, - ) - - def test_guard_configuration_contains_repository_errors_when_repository_missing_from_config(): flexmock(module).should_receive('repositories_match').replace_with( lambda first, second: first == second @@ -153,3 +145,30 @@ def test_guard_configuration_contains_repository_errors_when_repository_matches_ 'other.yaml': {'location': {'repositories': ['repo']}}, }, ) + + +def test_guard_single_repository_selected_raises_when_multiple_repositories_configured_and_none_selected(): + with pytest.raises(ValueError): + module.guard_single_repository_selected( + repository=None, + configurations={'config.yaml': {'location': {'repositories': ['repo', 'repo2']}}}, + ) + + +def test_guard_single_repository_selected_does_not_raise_when_single_repository_configured_and_none_selected(): + module.guard_single_repository_selected( + repository=None, configurations={'config.yaml': {'location': {'repositories': ['repo']}}}, + ) + + +def test_guard_single_repository_selected_does_not_raise_when_no_repositories_configured_and_one_selected(): + module.guard_single_repository_selected( + repository='repo', configurations={'config.yaml': {'location': {'repositories': []}}}, + ) + + +def test_guard_single_repository_selected_does_not_raise_when_repositories_configured_and_one_selected(): + module.guard_single_repository_selected( + repository='repo', + configurations={'config.yaml': {'location': {'repositories': ['repo', 'repo2']}}}, + ) From 22149c6401dcdf4d78508f83033783e52f07dced Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Mon, 1 Aug 2022 21:17:59 -0700 Subject: [PATCH 07/38] Switch to self-hosted container registry for borgmatic documentation image. --- .drone.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index a2e92ac95..cf4c358a1 100644 --- a/.drone.yml +++ b/.drone.yml @@ -42,7 +42,9 @@ steps: from_secret: docker_username password: from_secret: docker_password - repo: witten/borgmatic-docs + registry: projects.torsion.org + repo: projects.torsion.org/borgmatic-collective/borgmatic + tags: docs dockerfile: docs/Dockerfile trigger: From 622caa0c21f97761109cb7ac3153c10280391b13 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Fri, 12 Aug 2022 14:53:20 -0700 Subject: [PATCH 08/38] Support for Borg 2's rcreate and rinfo sub-commands (#557). --- NEWS | 3 +- borgmatic/borg/check.py | 8 +- borgmatic/borg/feature.py | 8 + borgmatic/borg/info.py | 28 ++- borgmatic/borg/{init.py => rcreate.py} | 40 ++-- borgmatic/commands/arguments.py | 37 ++-- borgmatic/commands/borgmatic.py | 17 +- docs/how-to/deal-with-very-large-backups.md | 7 +- docs/how-to/inspect-your-backups.md | 13 +- docs/how-to/set-up-backups.md | 30 +-- scripts/run-full-tests | 4 +- setup.py | 2 +- tests/integration/commands/test_arguments.py | 7 + tests/unit/borg/test_check.py | 42 +++-- tests/unit/borg/test_info.py | 106 +++++++++-- tests/unit/borg/test_init.py | 132 ------------- tests/unit/borg/test_rcreate.py | 183 +++++++++++++++++++ tests/unit/commands/test_borgmatic.py | 6 +- 18 files changed, 434 insertions(+), 239 deletions(-) rename borgmatic/borg/{init.py => rcreate.py} (57%) delete mode 100644 tests/unit/borg/test_init.py create mode 100644 tests/unit/borg/test_rcreate.py diff --git a/NEWS b/NEWS index f7002a315..5615aead5 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,5 @@ -1.6.7.dev0 +2.0.0.dev0 + * #557: Support for Borg 2 while still working with Borg 1. * #565: Fix handling of "repository" and "data" consistency checks to prevent invalid Borg flags. * #566: Modify "mount" and "extract" actions to require the "--repository" flag when multiple repositories are configured. diff --git a/borgmatic/borg/check.py b/borgmatic/borg/check.py index 665256fdc..ff3dfa978 100644 --- a/borgmatic/borg/check.py +++ b/borgmatic/borg/check.py @@ -5,7 +5,7 @@ import logging import os import pathlib -from borgmatic.borg import environment, extract, info, state +from borgmatic.borg import environment, extract, rinfo, state from borgmatic.execute import DO_NOT_CAPTURE, execute_command DEFAULT_CHECKS = ( @@ -241,6 +241,7 @@ def check_archives( location_config, storage_config, consistency_config, + local_borg_version, local_path='borg', remote_path=None, progress=None, @@ -260,10 +261,11 @@ def check_archives( ''' try: borg_repository_id = json.loads( - info.display_archives_info( + rinfo.display_repository_info( repository, storage_config, - argparse.Namespace(json=True, archive=None), + local_borg_version, + argparse.Namespace(json=True), local_path, remote_path, ) diff --git a/borgmatic/borg/feature.py b/borgmatic/borg/feature.py index af87d309e..0b77eb75b 100644 --- a/borgmatic/borg/feature.py +++ b/borgmatic/borg/feature.py @@ -9,6 +9,10 @@ class Feature(Enum): NOFLAGS = 3 NUMERIC_IDS = 4 UPLOAD_RATELIMIT = 5 + SEPARATE_REPOSITORY_ARCHIVE = 6 + RCREATE = 7 + RLIST = 8 + RINFO = 9 FEATURE_TO_MINIMUM_BORG_VERSION = { @@ -17,6 +21,10 @@ FEATURE_TO_MINIMUM_BORG_VERSION = { Feature.NOFLAGS: parse_version('1.2.0a8'), # borg create --noflags Feature.NUMERIC_IDS: parse_version('1.2.0b3'), # borg create/extract/mount --numeric-ids Feature.UPLOAD_RATELIMIT: parse_version('1.2.0b3'), # borg create --upload-ratelimit + Feature.SEPARATE_REPOSITORY_ARCHIVE: parse_version('2.0.0a2'), # --repo with separate archive + Feature.RCREATE: parse_version('2.0.0a2'), # borg rcreate + Feature.RLIST: parse_version('2.0.0a2'), # borg rlist + Feature.RINFO: parse_version('2.0.0a2'), # borg rinfo } diff --git a/borgmatic/borg/info.py b/borgmatic/borg/info.py index 6e783e88e..9e56f0ede 100644 --- a/borgmatic/borg/info.py +++ b/borgmatic/borg/info.py @@ -1,6 +1,6 @@ import logging -from borgmatic.borg import environment +from borgmatic.borg import environment, feature from borgmatic.borg.flags import make_flags, make_flags_from_arguments from borgmatic.execute import execute_command @@ -8,12 +8,17 @@ logger = logging.getLogger(__name__) def display_archives_info( - repository, storage_config, info_arguments, local_path='borg', remote_path=None + repository, + storage_config, + local_borg_version, + info_arguments, + local_path='borg', + remote_path=None, ): ''' - Given a local or remote repository path, a storage config dict, and the arguments to the info - action, display summary information for Borg archives in the repository or return JSON summary - information. + Given a local or remote repository path, a storage config dict, the local Borg version, and the + arguments to the info action, display summary information for Borg archives in the repository or + return JSON summary information. ''' lock_wait = storage_config.get('lock_wait', None) @@ -33,9 +38,16 @@ def display_archives_info( + make_flags('lock-wait', lock_wait) + make_flags_from_arguments(info_arguments, excludes=('repository', 'archive')) + ( - '::'.join((repository, info_arguments.archive)) - if info_arguments.archive - else repository, + ( + ('--repo', repository) + + (('--glob-archives', info_arguments.archive) if info_arguments.archive else ()) + ) + if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version) + else ( + '::'.join((repository, info_arguments.archive)) + if info_arguments.archive + else repository, + ) ) ) diff --git a/borgmatic/borg/init.py b/borgmatic/borg/rcreate.py similarity index 57% rename from borgmatic/borg/init.py rename to borgmatic/borg/rcreate.py index 9329bc1b0..8d3609379 100644 --- a/borgmatic/borg/init.py +++ b/borgmatic/borg/rcreate.py @@ -2,18 +2,19 @@ import argparse import logging import subprocess -from borgmatic.borg import environment, info +from borgmatic.borg import environment, feature, rinfo from borgmatic.execute import DO_NOT_CAPTURE, execute_command logger = logging.getLogger(__name__) -INFO_REPOSITORY_NOT_FOUND_EXIT_CODE = 2 +RINFO_REPOSITORY_NOT_FOUND_EXIT_CODE = 2 -def initialize_repository( +def create_repository( repository, storage_config, + local_borg_version, encryption_mode, append_only=None, storage_quota=None, @@ -21,28 +22,34 @@ def initialize_repository( remote_path=None, ): ''' - Given a local or remote repository path, a storage configuration dict, a Borg encryption mode, - whether the repository should be append-only, and the storage quota to use, initialize the - repository. If the repository already exists, then log and skip initialization. + Given a local or remote repository path, a storage configuration dict, the local Borg version, a + Borg encryption mode, whether the repository should be append-only, and the storage quota to + use, create the repository. If the repository already exists, then log and skip creation. ''' try: - info.display_archives_info( + rinfo.display_repository_info( repository, storage_config, - argparse.Namespace(json=True, archive=None), + local_borg_version, + argparse.Namespace(json=True), local_path, remote_path, ) - logger.info('Repository already exists. Skipping initialization.') + logger.info('Repository already exists. Skipping creation.') return except subprocess.CalledProcessError as error: - if error.returncode != INFO_REPOSITORY_NOT_FOUND_EXIT_CODE: + if error.returncode != RINFO_REPOSITORY_NOT_FOUND_EXIT_CODE: raise - extra_borg_options = storage_config.get('extra_borg_options', {}).get('init', '') + extra_borg_options = storage_config.get('extra_borg_options', {}).get('rcreate', '') - init_command = ( - (local_path, 'init') + rcreate_command = ( + (local_path,) + + ( + ('rcreate',) + if feature.available(feature.Feature.RCREATE, local_borg_version) + else ('init',) + ) + (('--encryption', encryption_mode) if encryption_mode else ()) + (('--append-only',) if append_only else ()) + (('--storage-quota', storage_quota) if storage_quota else ()) @@ -50,12 +57,17 @@ def initialize_repository( + (('--debug',) if logger.isEnabledFor(logging.DEBUG) else ()) + (('--remote-path', remote_path) if remote_path else ()) + (tuple(extra_borg_options.split(' ')) if extra_borg_options else ()) + + ( + ('--repo',) + if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version) + else () + ) + (repository,) ) # Do not capture output here, so as to support interactive prompts. execute_command( - init_command, + rcreate_command, output_file=DO_NOT_CAPTURE, borg_local_path=local_path, extra_environment=environment.make_environment(storage_config), diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index 46fcae4cc..736f22f11 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -4,7 +4,7 @@ from argparse import Action, ArgumentParser from borgmatic.config import collect SUBPARSER_ALIASES = { - 'init': ['--init', '-I'], + 'rcreate': ['init', '--init', '-I'], 'prune': ['--prune', '-p'], 'compact': [], 'create': ['--create', '-C'], @@ -222,33 +222,35 @@ def make_parsers(): metavar='', help='Specify zero or more actions. Defaults to prune, compact, create, and check. Use --help with action for details:', ) - init_parser = subparsers.add_parser( - 'init', - aliases=SUBPARSER_ALIASES['init'], - help='Initialize an empty Borg repository', - description='Initialize an empty Borg repository', + rcreate_parser = subparsers.add_parser( + 'rcreate', + aliases=SUBPARSER_ALIASES['rcreate'], + help='Create a new, empty Borg repository', + description='Create a new, empty Borg repository', add_help=False, ) - init_group = init_parser.add_argument_group('init arguments') - init_group.add_argument( + rcreate_group = rcreate_parser.add_argument_group('rcreate arguments') + rcreate_group.add_argument( '-e', '--encryption', dest='encryption_mode', help='Borg repository encryption mode', required=True, ) - init_group.add_argument( + rcreate_group.add_argument( '--append-only', dest='append_only', action='store_true', help='Create an append-only repository', ) - init_group.add_argument( + rcreate_group.add_argument( '--storage-quota', dest='storage_quota', help='Create a repository with a fixed storage quota', ) - init_group.add_argument('-h', '--help', action='help', help='Show this help message and exit') + rcreate_group.add_argument( + '-h', '--help', action='help', help='Show this help message and exit' + ) prune_parser = subparsers.add_parser( 'prune', @@ -688,11 +690,11 @@ def parse_arguments(*unparsed_arguments): if arguments['global'].excludes_filename: raise ValueError( - 'The --excludes option has been replaced with exclude_patterns in configuration' + 'The --excludes flag has been replaced with exclude_patterns in configuration' ) - if 'init' in arguments and arguments['global'].dry_run: - raise ValueError('The init action cannot be used with the --dry-run option') + if 'rcreate' in arguments and arguments['global'].dry_run: + raise ValueError('The rcreate/init action cannot be used with the --dry-run flag') if ( 'list' in arguments @@ -700,6 +702,11 @@ def parse_arguments(*unparsed_arguments): and arguments['list'].json and arguments['info'].json ): - raise ValueError('With the --json option, list and info actions cannot be used together') + raise ValueError('With the --json flag, list and info actions cannot be used together') + + if 'info' in arguments and arguments['info'].archive and arguments['info'].glob_archives: + raise ValueError( + 'With the info action, the --archive and --glob-archives flags cannot be used together' + ) return arguments diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index 2e11c97d6..494f41f85 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -20,10 +20,10 @@ from borgmatic.borg import export_tar as borg_export_tar from borgmatic.borg import extract as borg_extract from borgmatic.borg import feature as borg_feature from borgmatic.borg import info as borg_info -from borgmatic.borg import init as borg_init from borgmatic.borg import list as borg_list from borgmatic.borg import mount as borg_mount from borgmatic.borg import prune as borg_prune +from borgmatic.borg import rcreate as borg_rcreate from borgmatic.borg import umount as borg_umount from borgmatic.borg import version as borg_version from borgmatic.commands.arguments import parse_arguments @@ -249,14 +249,15 @@ def run_actions( 'repositories': ','.join(location['repositories']), } - if 'init' in arguments: - logger.info('{}: Initializing repository'.format(repository)) - borg_init.initialize_repository( + if 'rcreate' in arguments: + logger.info('{}: Creating repository'.format(repository)) + borg_rcreate.create_repository( repository, storage, - arguments['init'].encryption_mode, - arguments['init'].append_only, - arguments['init'].storage_quota, + local_borg_version, + arguments['rcreate'].encryption_mode, + arguments['rcreate'].append_only, + arguments['rcreate'].storage_quota, local_path=local_path, remote_path=remote_path, ) @@ -396,6 +397,7 @@ def run_actions( location, storage, consistency, + local_borg_version, local_path=local_path, remote_path=remote_path, progress=arguments['check'].progress, @@ -624,6 +626,7 @@ def run_actions( json_output = borg_info.display_archives_info( repository, storage, + local_borg_version, info_arguments=info_arguments, local_path=local_path, remote_path=remote_path, diff --git a/docs/how-to/deal-with-very-large-backups.md b/docs/how-to/deal-with-very-large-backups.md index aa67f1f63..8318ebc42 100644 --- a/docs/how-to/deal-with-very-large-backups.md +++ b/docs/how-to/deal-with-very-large-backups.md @@ -27,9 +27,6 @@ borgmatic create borgmatic check ``` -(No borgmatic `prune`, `create`, or `check` actions? Try the old-style -`--prune`, `--create`, or `--check`. Or upgrade borgmatic!) - You can run with only one of these actions provided, or you can mix and match any number of them in a single borgmatic run. This supports approaches like skipping certain actions while running others. For instance, this skips @@ -70,7 +67,9 @@ Here are the available checks from fastest to slowest: * `extract`: Performs an extraction dry-run of the most recent archive. * `data`: Verifies the data integrity of all archives contents, decrypting and decompressing all data (implies `archives` as well). -See [Borg's check documentation](https://borgbackup.readthedocs.io/en/stable/usage/check.html) for more information. +See [Borg's check +documentation](https://borgbackup.readthedocs.io/en/stable/usage/check.html) +for more information. ### Check frequency diff --git a/docs/how-to/inspect-your-backups.md b/docs/how-to/inspect-your-backups.md index 318eb6eed..b266745f2 100644 --- a/docs/how-to/inspect-your-backups.md +++ b/docs/how-to/inspect-your-backups.md @@ -37,19 +37,22 @@ borgmatic --stats ## Existing backups borgmatic provides convenient actions for Borg's -[list](https://borgbackup.readthedocs.io/en/stable/usage/list.html) and -[info](https://borgbackup.readthedocs.io/en/stable/usage/info.html) +[`list`](https://borgbackup.readthedocs.io/en/stable/usage/list.html) and +[`info`](https://borgbackup.readthedocs.io/en/stable/usage/info.html) functionality: - ```bash borgmatic list borgmatic info ``` -(No borgmatic `list` or `info` actions? Try the old-style `--list` or -`--info`. Or upgrade borgmatic!) +New in borgmatic version 2.0.0 +There's also an `rinfo` action for displaying repository information with Borg +2.x: +```bash +borgmatic rinfo +``` ### Searching for a file diff --git a/docs/how-to/set-up-backups.md b/docs/how-to/set-up-backups.md index 4206f6cab..6508cd69a 100644 --- a/docs/how-to/set-up-backups.md +++ b/docs/how-to/set-up-backups.md @@ -186,32 +186,36 @@ files via configuration management, or you want to double check that your hand edits are valid. -## Initialization +## Repository creation -Before you can create backups with borgmatic, you first need to initialize a -Borg repository so you have a destination for your backup archives. (But skip -this step if you already have a Borg repository.) To create a repository, run -a command like the following: +Before you can create backups with borgmatic, you first need to create a Borg +repository so you have a destination for your backup archives. (But skip this +step if you already have a Borg repository.) To create a repository, run a +command like the following with Borg 1.x: ```bash sudo borgmatic init --encryption repokey ``` -(No borgmatic `init` action? Try the old-style `--init` flag, or upgrade -borgmatic!) +New in borgmatic version 2.0.0 +Or, with Borg 2.x: + +```bash +sudo borgmatic rcreate --encryption repokey-aes-ocb +``` This uses the borgmatic configuration file you created above to determine which local or remote repository to create, and encrypts it with the encryption passphrase specified there if one is provided. Read about [Borg encryption -modes](https://borgbackup.readthedocs.io/en/stable/usage/init.html#encryption-modes) +modes](https://borgbackup.readthedocs.io/en/stable/usage/init.html#encryption-mode-tldr) for the menu of available encryption modes. Also, optionally check out the [Borg Quick Start](https://borgbackup.readthedocs.org/en/stable/quickstart.html) for more -background about repository initialization. +background about repository creation. -Note that borgmatic skips repository initialization if the repository already +Note that borgmatic skips repository creation if the repository already exists. This supports use cases like ensuring a repository exists prior to performing a backup. @@ -221,8 +225,8 @@ key-based SSH access to the desired user account on the remote host. ## Backups -Now that you've configured borgmatic and initialized a repository, it's a -good idea to test that borgmatic is working. So to run borgmatic and start a +Now that you've configured borgmatic and created a repository, it's a good +idea to test that borgmatic is working. So to run borgmatic and start a backup, you can invoke it like this: ```bash @@ -230,7 +234,7 @@ sudo borgmatic create --verbosity 1 --files --stats ``` (No borgmatic `--files` flag? It's only present in newer versions of -borgmatic. So try leaving it out, or upgrade borgmatic!) +borgmatic. So try leaving it out or upgrade borgmatic!) The `--verbosity` flag makes borgmatic show the steps it's performing. The `--files` flag lists each file that's new or changed since the last backup. diff --git a/scripts/run-full-tests b/scripts/run-full-tests index a54e0abdd..993c5bd89 100755 --- a/scripts/run-full-tests +++ b/scripts/run-full-tests @@ -14,8 +14,8 @@ apk add --no-cache python3 py3-pip borgbackup postgresql-client mariadb-client m py3-ruamel.yaml py3-ruamel.yaml.clib bash # If certain dependencies of black are available in this version of Alpine, install them. apk add --no-cache py3-typed-ast py3-regex || true -python3 -m pip install --no-cache --upgrade pip==22.0.3 setuptools==60.8.1 -pip3 install tox==3.24.5 +python3 -m pip install --no-cache --upgrade pip==22.2.2 setuptools==64.0.1 +pip3 install --ignore-installed tox==3.25.1 export COVERAGE_FILE=/tmp/.coverage tox --workdir /tmp/.tox --sitepackages tox --workdir /tmp/.tox --sitepackages -e end-to-end diff --git a/setup.py b/setup.py index 4b2c6ee9a..e577a25af 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import find_packages, setup -VERSION = '1.6.7.dev0' +VERSION = '2.0.0.dev0' setup( diff --git a/tests/integration/commands/test_arguments.py b/tests/integration/commands/test_arguments.py index e13a31b6b..5eb51013d 100644 --- a/tests/integration/commands/test_arguments.py +++ b/tests/integration/commands/test_arguments.py @@ -496,6 +496,13 @@ def test_parse_arguments_disallows_json_with_both_list_and_info(): module.parse_arguments('list', 'info', '--json') +def test_parse_arguments_disallows_info_with_both_archive_and_glob_archives(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(ValueError): + module.parse_arguments('info', '--archive', 'foo', '--glob-archives', '*bar') + + def test_parse_arguments_check_only_extract_does_not_raise_extract_subparser_error(): flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) diff --git a/tests/unit/borg/test_check.py b/tests/unit/borg/test_check.py index 94a913369..d56d31b02 100644 --- a/tests/unit/borg/test_check.py +++ b/tests/unit/borg/test_check.py @@ -296,7 +296,7 @@ def test_check_archives_with_progress_calls_borg_with_progress_parameter(): consistency_config = {'check_last': None} flexmock(module).should_receive('parse_checks') flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks) - flexmock(module.info).should_receive('display_archives_info').and_return( + flexmock(module.rinfo).should_receive('display_repository_info').and_return( '{"repository": {"id": "repo"}}' ) flexmock(module).should_receive('make_check_flags').and_return(()) @@ -315,6 +315,7 @@ def test_check_archives_with_progress_calls_borg_with_progress_parameter(): location_config={}, storage_config={}, consistency_config=consistency_config, + local_borg_version='1.2.3', progress=True, ) @@ -324,7 +325,7 @@ def test_check_archives_with_repair_calls_borg_with_repair_parameter(): consistency_config = {'check_last': None} flexmock(module).should_receive('parse_checks') flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks) - flexmock(module.info).should_receive('display_archives_info').and_return( + flexmock(module.rinfo).should_receive('display_repository_info').and_return( '{"repository": {"id": "repo"}}' ) flexmock(module).should_receive('make_check_flags').and_return(()) @@ -343,6 +344,7 @@ def test_check_archives_with_repair_calls_borg_with_repair_parameter(): location_config={}, storage_config={}, consistency_config=consistency_config, + local_borg_version='1.2.3', repair=True, ) @@ -361,7 +363,7 @@ def test_check_archives_calls_borg_with_parameters(checks): consistency_config = {'check_last': check_last} flexmock(module).should_receive('parse_checks') flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks) - flexmock(module.info).should_receive('display_archives_info').and_return( + flexmock(module.rinfo).should_receive('display_repository_info').and_return( '{"repository": {"id": "repo"}}' ) flexmock(module).should_receive('make_check_flags').with_args( @@ -376,6 +378,7 @@ def test_check_archives_calls_borg_with_parameters(checks): location_config={}, storage_config={}, consistency_config=consistency_config, + local_borg_version='1.2.3', ) @@ -385,7 +388,7 @@ def test_check_archives_with_json_error_raises(): consistency_config = {'check_last': check_last} flexmock(module).should_receive('parse_checks') flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks) - flexmock(module.info).should_receive('display_archives_info').and_return( + flexmock(module.rinfo).should_receive('display_repository_info').and_return( '{"unexpected": {"id": "repo"}}' ) @@ -395,6 +398,7 @@ def test_check_archives_with_json_error_raises(): location_config={}, storage_config={}, consistency_config=consistency_config, + local_borg_version='1.2.3', ) @@ -404,7 +408,7 @@ def test_check_archives_with_missing_json_keys_raises(): consistency_config = {'check_last': check_last} flexmock(module).should_receive('parse_checks') flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks) - flexmock(module.info).should_receive('display_archives_info').and_return('{invalid JSON') + flexmock(module.rinfo).should_receive('display_repository_info').and_return('{invalid JSON') with pytest.raises(ValueError): module.check_archives( @@ -412,6 +416,7 @@ def test_check_archives_with_missing_json_keys_raises(): location_config={}, storage_config={}, consistency_config=consistency_config, + local_borg_version='1.2.3', ) @@ -421,7 +426,7 @@ def test_check_archives_with_extract_check_calls_extract_only(): consistency_config = {'check_last': check_last} flexmock(module).should_receive('parse_checks') flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks) - flexmock(module.info).should_receive('display_archives_info').and_return( + flexmock(module.rinfo).should_receive('display_repository_info').and_return( '{"repository": {"id": "repo"}}' ) flexmock(module).should_receive('make_check_flags').never() @@ -434,6 +439,7 @@ def test_check_archives_with_extract_check_calls_extract_only(): location_config={}, storage_config={}, consistency_config=consistency_config, + local_borg_version='1.2.3', ) @@ -442,7 +448,7 @@ def test_check_archives_with_log_info_calls_borg_with_info_parameter(): consistency_config = {'check_last': None} flexmock(module).should_receive('parse_checks') flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks) - flexmock(module.info).should_receive('display_archives_info').and_return( + flexmock(module.rinfo).should_receive('display_repository_info').and_return( '{"repository": {"id": "repo"}}' ) flexmock(module).should_receive('make_check_flags').and_return(()) @@ -456,6 +462,7 @@ def test_check_archives_with_log_info_calls_borg_with_info_parameter(): location_config={}, storage_config={}, consistency_config=consistency_config, + local_borg_version='1.2.3', ) @@ -464,7 +471,7 @@ def test_check_archives_with_log_debug_calls_borg_with_debug_parameter(): consistency_config = {'check_last': None} flexmock(module).should_receive('parse_checks') flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks) - flexmock(module.info).should_receive('display_archives_info').and_return( + flexmock(module.rinfo).should_receive('display_repository_info').and_return( '{"repository": {"id": "repo"}}' ) flexmock(module).should_receive('make_check_flags').and_return(()) @@ -478,6 +485,7 @@ def test_check_archives_with_log_debug_calls_borg_with_debug_parameter(): location_config={}, storage_config={}, consistency_config=consistency_config, + local_borg_version='1.2.3', ) @@ -485,7 +493,7 @@ def test_check_archives_without_any_checks_bails(): consistency_config = {'check_last': None} flexmock(module).should_receive('parse_checks') flexmock(module).should_receive('filter_checks_on_frequency').and_return(()) - flexmock(module.info).should_receive('display_archives_info').and_return( + flexmock(module.rinfo).should_receive('display_repository_info').and_return( '{"repository": {"id": "repo"}}' ) insert_execute_command_never() @@ -495,6 +503,7 @@ def test_check_archives_without_any_checks_bails(): location_config={}, storage_config={}, consistency_config=consistency_config, + local_borg_version='1.2.3', ) @@ -504,7 +513,7 @@ def test_check_archives_with_local_path_calls_borg_via_local_path(): consistency_config = {'check_last': check_last} flexmock(module).should_receive('parse_checks') flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks) - flexmock(module.info).should_receive('display_archives_info').and_return( + flexmock(module.rinfo).should_receive('display_repository_info').and_return( '{"repository": {"id": "repo"}}' ) flexmock(module).should_receive('make_check_flags').with_args( @@ -519,6 +528,7 @@ def test_check_archives_with_local_path_calls_borg_via_local_path(): location_config={}, storage_config={}, consistency_config=consistency_config, + local_borg_version='1.2.3', local_path='borg1', ) @@ -529,7 +539,7 @@ def test_check_archives_with_remote_path_calls_borg_with_remote_path_parameters( consistency_config = {'check_last': check_last} flexmock(module).should_receive('parse_checks') flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks) - flexmock(module.info).should_receive('display_archives_info').and_return( + flexmock(module.rinfo).should_receive('display_repository_info').and_return( '{"repository": {"id": "repo"}}' ) flexmock(module).should_receive('make_check_flags').with_args( @@ -544,6 +554,7 @@ def test_check_archives_with_remote_path_calls_borg_with_remote_path_parameters( location_config={}, storage_config={}, consistency_config=consistency_config, + local_borg_version='1.2.3', remote_path='borg1', ) @@ -554,7 +565,7 @@ def test_check_archives_with_lock_wait_calls_borg_with_lock_wait_parameters(): consistency_config = {'check_last': check_last} flexmock(module).should_receive('parse_checks') flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks) - flexmock(module.info).should_receive('display_archives_info').and_return( + flexmock(module.rinfo).should_receive('display_repository_info').and_return( '{"repository": {"id": "repo"}}' ) flexmock(module).should_receive('make_check_flags').with_args( @@ -569,6 +580,7 @@ def test_check_archives_with_lock_wait_calls_borg_with_lock_wait_parameters(): location_config={}, storage_config={'lock_wait': 5}, consistency_config=consistency_config, + local_borg_version='1.2.3', ) @@ -579,7 +591,7 @@ def test_check_archives_with_retention_prefix(): consistency_config = {'check_last': check_last, 'prefix': prefix} flexmock(module).should_receive('parse_checks') flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks) - flexmock(module.info).should_receive('display_archives_info').and_return( + flexmock(module.rinfo).should_receive('display_repository_info').and_return( '{"repository": {"id": "repo"}}' ) flexmock(module).should_receive('make_check_flags').with_args( @@ -594,6 +606,7 @@ def test_check_archives_with_retention_prefix(): location_config={}, storage_config={}, consistency_config=consistency_config, + local_borg_version='1.2.3', ) @@ -602,7 +615,7 @@ def test_check_archives_with_extra_borg_options_calls_borg_with_extra_options(): consistency_config = {'check_last': None} flexmock(module).should_receive('parse_checks') flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks) - flexmock(module.info).should_receive('display_archives_info').and_return( + flexmock(module.rinfo).should_receive('display_repository_info').and_return( '{"repository": {"id": "repo"}}' ) flexmock(module).should_receive('make_check_flags').and_return(()) @@ -615,4 +628,5 @@ def test_check_archives_with_extra_borg_options_calls_borg_with_extra_options(): location_config={}, storage_config={'extra_borg_options': {'check': '--extra --options'}}, consistency_config=consistency_config, + local_borg_version='1.2.3', ) diff --git a/tests/unit/borg/test_info.py b/tests/unit/borg/test_info.py index b91f56094..8817bb7ee 100644 --- a/tests/unit/borg/test_info.py +++ b/tests/unit/borg/test_info.py @@ -9,6 +9,25 @@ from ..test_verbosity import insert_logging_mock def test_display_archives_info_calls_borg_with_parameters(): + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'info', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.display_archives_info( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + info_arguments=flexmock(archive=None, json=False), + ) + + +def test_display_archives_info_without_borg_features_calls_borg_without_repo_flag(): + flexmock(module.feature).should_receive('available').and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'info', 'repo'), @@ -18,28 +37,36 @@ def test_display_archives_info_calls_borg_with_parameters(): ) module.display_archives_info( - repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=False) + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + info_arguments=flexmock(archive=None, json=False), ) def test_display_archives_info_with_log_info_calls_borg_with_info_parameter(): + flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'info', '--info', 'repo'), + ('borg', 'info', '--info', '--repo', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg', extra_environment=None, ) insert_logging_mock(logging.INFO) module.display_archives_info( - repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=False) + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + info_arguments=flexmock(archive=None, json=False), ) def test_display_archives_info_with_log_info_and_json_suppresses_most_borg_output(): + flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'info', '--json', 'repo'), + ('borg', 'info', '--json', '--repo', 'repo'), output_log_level=None, borg_local_path='borg', extra_environment=None, @@ -47,16 +74,20 @@ def test_display_archives_info_with_log_info_and_json_suppresses_most_borg_outpu insert_logging_mock(logging.INFO) json_output = module.display_archives_info( - repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=True) + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + info_arguments=flexmock(archive=None, json=True), ) assert json_output == '[]' def test_display_archives_info_with_log_debug_calls_borg_with_debug_parameter(): + flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'info', '--debug', '--show-rc', 'repo'), + ('borg', 'info', '--debug', '--show-rc', '--repo', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg', extra_environment=None, @@ -64,14 +95,18 @@ def test_display_archives_info_with_log_debug_calls_borg_with_debug_parameter(): insert_logging_mock(logging.DEBUG) module.display_archives_info( - repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=False) + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + info_arguments=flexmock(archive=None, json=False), ) def test_display_archives_info_with_log_debug_and_json_suppresses_most_borg_output(): + flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'info', '--json', 'repo'), + ('borg', 'info', '--json', '--repo', 'repo'), output_log_level=None, borg_local_path='borg', extra_environment=None, @@ -79,29 +114,55 @@ def test_display_archives_info_with_log_debug_and_json_suppresses_most_borg_outp insert_logging_mock(logging.DEBUG) json_output = module.display_archives_info( - repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=True) + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + info_arguments=flexmock(archive=None, json=True), ) assert json_output == '[]' def test_display_archives_info_with_json_calls_borg_with_json_parameter(): + flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'info', '--json', 'repo'), + ('borg', 'info', '--json', '--repo', 'repo'), output_log_level=None, borg_local_path='borg', extra_environment=None, ).and_return('[]') json_output = module.display_archives_info( - repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=True) + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + info_arguments=flexmock(archive=None, json=True), ) assert json_output == '[]' -def test_display_archives_info_with_archive_calls_borg_with_archive_parameter(): +def test_display_archives_info_with_archive_calls_borg_with_glob_archives_parameter(): + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'info', '--repo', 'repo', '--glob-archives', 'archive'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.display_archives_info( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + info_arguments=flexmock(archive='archive', json=False), + ) + + +def test_display_archives_info_with_archive_and_without_borg_features_calls_borg_with_repo_archive_parameter(): + flexmock(module.feature).should_receive('available').and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'info', 'repo::archive'), @@ -111,14 +172,18 @@ def test_display_archives_info_with_archive_calls_borg_with_archive_parameter(): ) module.display_archives_info( - repository='repo', storage_config={}, info_arguments=flexmock(archive='archive', json=False) + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + info_arguments=flexmock(archive='archive', json=False), ) def test_display_archives_info_with_local_path_calls_borg_via_local_path(): + flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg1', 'info', 'repo'), + ('borg1', 'info', '--repo', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg1', extra_environment=None, @@ -127,15 +192,17 @@ def test_display_archives_info_with_local_path_calls_borg_via_local_path(): module.display_archives_info( repository='repo', storage_config={}, + local_borg_version='2.3.4', info_arguments=flexmock(archive=None, json=False), local_path='borg1', ) def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_parameters(): + flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'info', '--remote-path', 'borg1', 'repo'), + ('borg', 'info', '--remote-path', 'borg1', '--repo', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg', extra_environment=None, @@ -144,6 +211,7 @@ def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_para module.display_archives_info( repository='repo', storage_config={}, + local_borg_version='2.3.4', info_arguments=flexmock(archive=None, json=False), remote_path='borg1', ) @@ -151,9 +219,10 @@ def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_para def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_parameters(): storage_config = {'lock_wait': 5} + flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'info', '--lock-wait', '5', 'repo'), + ('borg', 'info', '--lock-wait', '5', '--repo', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg', extra_environment=None, @@ -162,15 +231,17 @@ def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_paramete module.display_archives_info( repository='repo', storage_config=storage_config, + local_borg_version='2.3.4', info_arguments=flexmock(archive=None, json=False), ) @pytest.mark.parametrize('argument_name', ('prefix', 'glob_archives', 'sort_by', 'first', 'last')) def test_display_archives_info_passes_through_arguments_to_borg(argument_name): + flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'info', '--' + argument_name.replace('_', '-'), 'value', 'repo'), + ('borg', 'info', '--' + argument_name.replace('_', '-'), 'value', '--repo', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg', extra_environment=None, @@ -179,5 +250,6 @@ def test_display_archives_info_passes_through_arguments_to_borg(argument_name): module.display_archives_info( repository='repo', storage_config={}, + local_borg_version='2.3.4', info_arguments=flexmock(archive=None, json=False, **{argument_name: 'value'}), ) diff --git a/tests/unit/borg/test_init.py b/tests/unit/borg/test_init.py deleted file mode 100644 index fb81f344c..000000000 --- a/tests/unit/borg/test_init.py +++ /dev/null @@ -1,132 +0,0 @@ -import logging -import subprocess - -import pytest -from flexmock import flexmock - -from borgmatic.borg import init as module - -from ..test_verbosity import insert_logging_mock - -INFO_SOME_UNKNOWN_EXIT_CODE = -999 -INIT_COMMAND = ('borg', 'init', '--encryption', 'repokey') - - -def insert_info_command_found_mock(): - flexmock(module.info).should_receive('display_archives_info') - - -def insert_info_command_not_found_mock(): - flexmock(module.info).should_receive('display_archives_info').and_raise( - subprocess.CalledProcessError(module.INFO_REPOSITORY_NOT_FOUND_EXIT_CODE, []) - ) - - -def insert_init_command_mock(init_command, **kwargs): - flexmock(module.environment).should_receive('make_environment') - flexmock(module).should_receive('execute_command').with_args( - init_command, - output_file=module.DO_NOT_CAPTURE, - borg_local_path=init_command[0], - extra_environment=None, - ).once() - - -def test_initialize_repository_calls_borg_with_parameters(): - insert_info_command_not_found_mock() - insert_init_command_mock(INIT_COMMAND + ('repo',)) - - module.initialize_repository(repository='repo', storage_config={}, encryption_mode='repokey') - - -def test_initialize_repository_raises_for_borg_init_error(): - insert_info_command_not_found_mock() - flexmock(module.environment).should_receive('make_environment') - flexmock(module).should_receive('execute_command').and_raise( - module.subprocess.CalledProcessError(2, 'borg init') - ) - - with pytest.raises(subprocess.CalledProcessError): - module.initialize_repository( - repository='repo', storage_config={}, encryption_mode='repokey' - ) - - -def test_initialize_repository_skips_initialization_when_repository_already_exists(): - insert_info_command_found_mock() - - module.initialize_repository(repository='repo', storage_config={}, encryption_mode='repokey') - - -def test_initialize_repository_raises_for_unknown_info_command_error(): - flexmock(module.info).should_receive('display_archives_info').and_raise( - subprocess.CalledProcessError(INFO_SOME_UNKNOWN_EXIT_CODE, []) - ) - - with pytest.raises(subprocess.CalledProcessError): - module.initialize_repository( - repository='repo', storage_config={}, encryption_mode='repokey' - ) - - -def test_initialize_repository_with_append_only_calls_borg_with_append_only_parameter(): - insert_info_command_not_found_mock() - insert_init_command_mock(INIT_COMMAND + ('--append-only', 'repo')) - - module.initialize_repository( - repository='repo', storage_config={}, encryption_mode='repokey', append_only=True - ) - - -def test_initialize_repository_with_storage_quota_calls_borg_with_storage_quota_parameter(): - insert_info_command_not_found_mock() - insert_init_command_mock(INIT_COMMAND + ('--storage-quota', '5G', 'repo')) - - module.initialize_repository( - repository='repo', storage_config={}, encryption_mode='repokey', storage_quota='5G' - ) - - -def test_initialize_repository_with_log_info_calls_borg_with_info_parameter(): - insert_info_command_not_found_mock() - insert_init_command_mock(INIT_COMMAND + ('--info', 'repo')) - insert_logging_mock(logging.INFO) - - module.initialize_repository(repository='repo', storage_config={}, encryption_mode='repokey') - - -def test_initialize_repository_with_log_debug_calls_borg_with_debug_parameter(): - insert_info_command_not_found_mock() - insert_init_command_mock(INIT_COMMAND + ('--debug', 'repo')) - insert_logging_mock(logging.DEBUG) - - module.initialize_repository(repository='repo', storage_config={}, encryption_mode='repokey') - - -def test_initialize_repository_with_local_path_calls_borg_via_local_path(): - insert_info_command_not_found_mock() - insert_init_command_mock(('borg1',) + INIT_COMMAND[1:] + ('repo',)) - - module.initialize_repository( - repository='repo', storage_config={}, encryption_mode='repokey', local_path='borg1' - ) - - -def test_initialize_repository_with_remote_path_calls_borg_with_remote_path_parameter(): - insert_info_command_not_found_mock() - insert_init_command_mock(INIT_COMMAND + ('--remote-path', 'borg1', 'repo')) - - module.initialize_repository( - repository='repo', storage_config={}, encryption_mode='repokey', remote_path='borg1' - ) - - -def test_initialize_repository_with_extra_borg_options_calls_borg_with_extra_options(): - insert_info_command_not_found_mock() - insert_init_command_mock(INIT_COMMAND + ('--extra', '--options', 'repo')) - - module.initialize_repository( - repository='repo', - storage_config={'extra_borg_options': {'init': '--extra --options'}}, - encryption_mode='repokey', - ) diff --git a/tests/unit/borg/test_rcreate.py b/tests/unit/borg/test_rcreate.py new file mode 100644 index 000000000..652de7b21 --- /dev/null +++ b/tests/unit/borg/test_rcreate.py @@ -0,0 +1,183 @@ +import logging +import subprocess + +import pytest +from flexmock import flexmock + +from borgmatic.borg import rcreate as module + +from ..test_verbosity import insert_logging_mock + +RINFO_SOME_UNKNOWN_EXIT_CODE = -999 +RCREATE_COMMAND = ('borg', 'rcreate', '--encryption', 'repokey') + + +def insert_rinfo_command_found_mock(): + flexmock(module.rinfo).should_receive('display_repository_info') + + +def insert_rinfo_command_not_found_mock(): + flexmock(module.rinfo).should_receive('display_repository_info').and_raise( + subprocess.CalledProcessError(module.RINFO_REPOSITORY_NOT_FOUND_EXIT_CODE, []) + ) + + +def insert_rcreate_command_mock(rcreate_command, **kwargs): + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + rcreate_command, + output_file=module.DO_NOT_CAPTURE, + borg_local_path=rcreate_command[0], + extra_environment=None, + ).once() + + +def test_create_repository_calls_borg_with_parameters(): + insert_rinfo_command_not_found_mock() + insert_rcreate_command_mock(RCREATE_COMMAND + ('--repo', 'repo')) + flexmock(module.feature).should_receive('available').and_return(True) + + module.create_repository( + repository='repo', storage_config={}, local_borg_version='2.3.4', encryption_mode='repokey' + ) + + +def test_create_repository_without_borg_features_calls_borg_with_init_sub_command(): + insert_rinfo_command_not_found_mock() + insert_rcreate_command_mock(('borg', 'init', '--encryption', 'repokey', 'repo')) + flexmock(module.feature).should_receive('available').and_return(False) + + module.create_repository( + repository='repo', storage_config={}, local_borg_version='2.3.4', encryption_mode='repokey' + ) + + +def test_create_repository_raises_for_borg_rcreate_error(): + insert_rinfo_command_not_found_mock() + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').and_raise( + module.subprocess.CalledProcessError(2, 'borg rcreate') + ) + + with pytest.raises(subprocess.CalledProcessError): + module.create_repository( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', + ) + + +def test_create_repository_skips_creation_when_repository_already_exists(): + insert_rinfo_command_found_mock() + flexmock(module.feature).should_receive('available').and_return(True) + + module.create_repository( + repository='repo', storage_config={}, local_borg_version='2.3.4', encryption_mode='repokey' + ) + + +def test_create_repository_raises_for_unknown_rinfo_command_error(): + flexmock(module.rinfo).should_receive('display_repository_info').and_raise( + subprocess.CalledProcessError(RINFO_SOME_UNKNOWN_EXIT_CODE, []) + ) + + with pytest.raises(subprocess.CalledProcessError): + module.create_repository( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', + ) + + +def test_create_repository_with_append_only_calls_borg_with_append_only_parameter(): + insert_rinfo_command_not_found_mock() + insert_rcreate_command_mock(RCREATE_COMMAND + ('--append-only', '--repo', 'repo')) + flexmock(module.feature).should_receive('available').and_return(True) + + module.create_repository( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', + append_only=True, + ) + + +def test_create_repository_with_storage_quota_calls_borg_with_storage_quota_parameter(): + insert_rinfo_command_not_found_mock() + insert_rcreate_command_mock(RCREATE_COMMAND + ('--storage-quota', '5G', '--repo', 'repo')) + flexmock(module.feature).should_receive('available').and_return(True) + + module.create_repository( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', + storage_quota='5G', + ) + + +def test_create_repository_with_log_info_calls_borg_with_info_parameter(): + insert_rinfo_command_not_found_mock() + insert_rcreate_command_mock(RCREATE_COMMAND + ('--info', '--repo', 'repo')) + insert_logging_mock(logging.INFO) + flexmock(module.feature).should_receive('available').and_return(True) + + module.create_repository( + repository='repo', storage_config={}, local_borg_version='2.3.4', encryption_mode='repokey' + ) + + +def test_create_repository_with_log_debug_calls_borg_with_debug_parameter(): + insert_rinfo_command_not_found_mock() + insert_rcreate_command_mock(RCREATE_COMMAND + ('--debug', '--repo', 'repo')) + insert_logging_mock(logging.DEBUG) + flexmock(module.feature).should_receive('available').and_return(True) + + module.create_repository( + repository='repo', storage_config={}, local_borg_version='2.3.4', encryption_mode='repokey' + ) + + +def test_create_repository_with_local_path_calls_borg_via_local_path(): + insert_rinfo_command_not_found_mock() + insert_rcreate_command_mock(('borg1',) + RCREATE_COMMAND[1:] + ('--repo', 'repo')) + flexmock(module.feature).should_receive('available').and_return(True) + + module.create_repository( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', + local_path='borg1', + ) + + +def test_create_repository_with_remote_path_calls_borg_with_remote_path_parameter(): + insert_rinfo_command_not_found_mock() + insert_rcreate_command_mock(RCREATE_COMMAND + ('--remote-path', 'borg1', '--repo', 'repo')) + flexmock(module.feature).should_receive('available').and_return(True) + + module.create_repository( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', + remote_path='borg1', + ) + + +def test_create_repository_with_extra_borg_options_calls_borg_with_extra_options(): + insert_rinfo_command_not_found_mock() + insert_rcreate_command_mock(RCREATE_COMMAND + ('--extra', '--options', '--repo', 'repo')) + flexmock(module.feature).should_receive('available').and_return(True) + + module.create_repository( + repository='repo', + storage_config={'extra_borg_options': {'rcreate': '--extra --options'}}, + local_borg_version='2.3.4', + encryption_mode='repokey', + ) diff --git a/tests/unit/commands/test_borgmatic.py b/tests/unit/commands/test_borgmatic.py index 66c5abefd..57ce1b4f6 100644 --- a/tests/unit/commands/test_borgmatic.py +++ b/tests/unit/commands/test_borgmatic.py @@ -340,11 +340,11 @@ def test_run_configuration_retries_timeout_multiple_repos(): assert results == error_logs -def test_run_actions_does_not_raise_for_init_action(): - flexmock(module.borg_init).should_receive('initialize_repository') +def test_run_actions_does_not_raise_for_rcreate_action(): + flexmock(module.borg_rcreate).should_receive('create_repository') arguments = { 'global': flexmock(monitoring_verbosity=1, dry_run=False), - 'init': flexmock( + 'rcreate': flexmock( encryption_mode=flexmock(), append_only=flexmock(), storage_quota=flexmock() ), } From e7a8acfb9645ee14baed5de67ff8a23d966da673 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Fri, 12 Aug 2022 14:59:03 -0700 Subject: [PATCH 09/38] Add missing rinfo action source files (#557). --- borgmatic/borg/rinfo.py | 58 ++++++++++ tests/unit/borg/test_rinfo.py | 199 ++++++++++++++++++++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 borgmatic/borg/rinfo.py create mode 100644 tests/unit/borg/test_rinfo.py diff --git a/borgmatic/borg/rinfo.py b/borgmatic/borg/rinfo.py new file mode 100644 index 000000000..ca771daa1 --- /dev/null +++ b/borgmatic/borg/rinfo.py @@ -0,0 +1,58 @@ +import logging + +from borgmatic.borg import environment, feature +from borgmatic.borg.flags import make_flags +from borgmatic.execute import execute_command + +logger = logging.getLogger(__name__) + + +def display_repository_info( + repository, + storage_config, + local_borg_version, + rinfo_arguments, + local_path='borg', + remote_path=None, +): + ''' + Given a local or remote repository path, a storage config dict, the local Borg version, and the + arguments to the rinfo action, display summary information for the Borg repository or return + JSON summary information. + ''' + lock_wait = storage_config.get('lock_wait', None) + + full_command = ( + (local_path,) + + ( + ('rinfo',) + if feature.available(feature.Feature.RINFO, local_borg_version) + else ('info',) + ) + + ( + ('--info',) + if logger.getEffectiveLevel() == logging.INFO and not rinfo_arguments.json + else () + ) + + ( + ('--debug', '--show-rc') + if logger.isEnabledFor(logging.DEBUG) and not rinfo_arguments.json + else () + ) + + make_flags('remote-path', remote_path) + + make_flags('lock-wait', lock_wait) + + (('--json',) if rinfo_arguments.json else ()) + + ( + ('--repo',) + if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version) + else () + ) + + (repository,) + ) + + return execute_command( + full_command, + output_log_level=None if rinfo_arguments.json else logging.WARNING, + borg_local_path=local_path, + extra_environment=environment.make_environment(storage_config), + ) diff --git a/tests/unit/borg/test_rinfo.py b/tests/unit/borg/test_rinfo.py new file mode 100644 index 000000000..93f91c0ea --- /dev/null +++ b/tests/unit/borg/test_rinfo.py @@ -0,0 +1,199 @@ +import logging + +from flexmock import flexmock + +from borgmatic.borg import rinfo as module + +from ..test_verbosity import insert_logging_mock + + +def test_display_repository_info_calls_borg_with_parameters(): + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'rinfo', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.display_repository_info( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + rinfo_arguments=flexmock(json=False), + ) + + +def test_display_repository_info_without_borg_features_calls_borg_with_info_sub_command(): + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'info', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.display_repository_info( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + rinfo_arguments=flexmock(json=False), + ) + + +def test_display_repository_info_with_log_info_calls_borg_with_info_parameter(): + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'rinfo', '--info', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + insert_logging_mock(logging.INFO) + module.display_repository_info( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + rinfo_arguments=flexmock(json=False), + ) + + +def test_display_repository_info_with_log_info_and_json_suppresses_most_borg_output(): + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'rinfo', '--json', '--repo', 'repo'), + output_log_level=None, + borg_local_path='borg', + extra_environment=None, + ).and_return('[]') + + insert_logging_mock(logging.INFO) + json_output = module.display_repository_info( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + rinfo_arguments=flexmock(json=True), + ) + + assert json_output == '[]' + + +def test_display_repository_info_with_log_debug_calls_borg_with_debug_parameter(): + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'rinfo', '--debug', '--show-rc', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + insert_logging_mock(logging.DEBUG) + + module.display_repository_info( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + rinfo_arguments=flexmock(json=False), + ) + + +def test_display_repository_info_with_log_debug_and_json_suppresses_most_borg_output(): + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'rinfo', '--json', '--repo', 'repo'), + output_log_level=None, + borg_local_path='borg', + extra_environment=None, + ).and_return('[]') + + insert_logging_mock(logging.DEBUG) + json_output = module.display_repository_info( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + rinfo_arguments=flexmock(json=True), + ) + + assert json_output == '[]' + + +def test_display_repository_info_with_json_calls_borg_with_json_parameter(): + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'rinfo', '--json', '--repo', 'repo'), + output_log_level=None, + borg_local_path='borg', + extra_environment=None, + ).and_return('[]') + + json_output = module.display_repository_info( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + rinfo_arguments=flexmock(json=True), + ) + + assert json_output == '[]' + + +def test_display_repository_info_with_local_path_calls_borg_via_local_path(): + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg1', 'rinfo', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg1', + extra_environment=None, + ) + + module.display_repository_info( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + rinfo_arguments=flexmock(json=False), + local_path='borg1', + ) + + +def test_display_repository_info_with_remote_path_calls_borg_with_remote_path_parameters(): + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'rinfo', '--remote-path', 'borg1', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.display_repository_info( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + rinfo_arguments=flexmock(json=False), + remote_path='borg1', + ) + + +def test_display_repository_info_with_lock_wait_calls_borg_with_lock_wait_parameters(): + storage_config = {'lock_wait': 5} + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'rinfo', '--lock-wait', '5', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.display_repository_info( + repository='repo', + storage_config=storage_config, + local_borg_version='2.3.4', + rinfo_arguments=flexmock(json=False), + ) From 647ecdac2939e183c9979855cc135e978294862f Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Fri, 12 Aug 2022 15:46:33 -0700 Subject: [PATCH 10/38] Borg 2 support in borgmatic check action (#557). --- borgmatic/borg/check.py | 17 +++++++--- borgmatic/commands/arguments.py | 2 +- tests/unit/borg/test_check.py | 55 +++++++++++++++++++++++++++------ 3 files changed, 59 insertions(+), 15 deletions(-) diff --git a/borgmatic/borg/check.py b/borgmatic/borg/check.py index ff3dfa978..0cdf90f62 100644 --- a/borgmatic/borg/check.py +++ b/borgmatic/borg/check.py @@ -5,7 +5,7 @@ import logging import os import pathlib -from borgmatic.borg import environment, extract, rinfo, state +from borgmatic.borg import environment, extract, feature, rinfo, state from borgmatic.execute import DO_NOT_CAPTURE, execute_command DEFAULT_CHECKS = ( @@ -163,14 +163,14 @@ def make_check_flags(checks, check_last=None, prefix=None): Additionally, if a check_last value is given and "archives" is in checks, then include a "--last" flag. And if a prefix value is given and "archives" is in checks, then include a - "--prefix" flag. + "--glob-archives" flag. ''' if 'archives' in checks: last_flags = ('--last', str(check_last)) if check_last else () - prefix_flags = ('--prefix', prefix) if prefix else () + glob_archives_flags = ('--glob-archives', f'{prefix}*') if prefix else () else: last_flags = () - prefix_flags = () + glob_archives_flags = () if check_last: logger.info('Ignoring check_last option, as "archives" is not in consistency checks') if prefix: @@ -184,7 +184,7 @@ def make_check_flags(checks, check_last=None, prefix=None): else: data_flags = () - common_flags = last_flags + prefix_flags + data_flags + common_flags = last_flags + glob_archives_flags + data_flags if {'repository', 'archives'}.issubset(set(checks)): return common_flags @@ -304,6 +304,13 @@ def check_archives( + verbosity_flags + (('--progress',) if progress else ()) + (tuple(extra_borg_options.split(' ')) if extra_borg_options else ()) + + ( + ('--repo',) + if feature.available( + feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version + ) + else () + ) + (repository,) ) diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index 736f22f11..052240640 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -292,7 +292,7 @@ def make_parsers(): dest='cleanup_commits', default=False, action='store_true', - help='Cleanup commit-only 17-byte segment files left behind by Borg 1.1', + help='Cleanup commit-only 17-byte segment files left behind by Borg 1.1 (flag in Borg 1.2 only)', ) compact_group.add_argument( '--threshold', diff --git a/tests/unit/borg/test_check.py b/tests/unit/borg/test_check.py index d56d31b02..60e416cfb 100644 --- a/tests/unit/borg/test_check.py +++ b/tests/unit/borg/test_check.py @@ -220,7 +220,7 @@ def test_make_check_flags_with_repository_and_data_checks_does_not_return_reposi def test_make_check_flags_with_default_checks_and_default_prefix_returns_default_flags(): flags = module.make_check_flags(('repository', 'archives'), prefix=module.DEFAULT_PREFIX) - assert flags == ('--prefix', module.DEFAULT_PREFIX) + assert flags == ('--glob-archives', f'{module.DEFAULT_PREFIX}*') def test_make_check_flags_with_all_checks_and_default_prefix_returns_default_flags(): @@ -228,7 +228,7 @@ def test_make_check_flags_with_all_checks_and_default_prefix_returns_default_fla ('repository', 'archives', 'extract'), prefix=module.DEFAULT_PREFIX ) - assert flags == ('--prefix', module.DEFAULT_PREFIX) + assert flags == ('--glob-archives', f'{module.DEFAULT_PREFIX}*') def test_make_check_flags_with_archives_check_and_last_includes_last_flag(): @@ -249,34 +249,34 @@ def test_make_check_flags_with_default_checks_and_last_includes_last_flag(): assert flags == ('--last', '3') -def test_make_check_flags_with_archives_check_and_prefix_includes_prefix_flag(): +def test_make_check_flags_with_archives_check_and_prefix_includes_glob_archives_flag(): flags = module.make_check_flags(('archives',), prefix='foo-') - assert flags == ('--archives-only', '--prefix', 'foo-') + assert flags == ('--archives-only', '--glob-archives', 'foo-*') -def test_make_check_flags_with_archives_check_and_empty_prefix_omits_prefix_flag(): +def test_make_check_flags_with_archives_check_and_empty_prefix_omits_glob_archives_flag(): flags = module.make_check_flags(('archives',), prefix='') assert flags == ('--archives-only',) -def test_make_check_flags_with_archives_check_and_none_prefix_omits_prefix_flag(): +def test_make_check_flags_with_archives_check_and_none_prefix_omits_glob_archives_flag(): flags = module.make_check_flags(('archives',), prefix=None) assert flags == ('--archives-only',) -def test_make_check_flags_with_repository_check_and_prefix_omits_prefix_flag(): +def test_make_check_flags_with_repository_check_and_prefix_omits_glob_archives_flag(): flags = module.make_check_flags(('repository',), prefix='foo-') assert flags == ('--repository-only',) -def test_make_check_flags_with_default_checks_and_prefix_includes_prefix_flag(): +def test_make_check_flags_with_default_checks_and_prefix_includes_glob_archives_flag(): flags = module.make_check_flags(('repository', 'archives'), prefix='foo-') - assert flags == ('--prefix', 'foo-') + assert flags == ('--glob-archives', 'foo-*') def test_read_check_time_does_not_raise(): @@ -301,6 +301,7 @@ def test_check_archives_with_progress_calls_borg_with_progress_parameter(): ) flexmock(module).should_receive('make_check_flags').and_return(()) flexmock(module).should_receive('execute_command').never() + flexmock(module.feature).should_receive('available').and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'check', '--progress', 'repo'), @@ -330,6 +331,7 @@ def test_check_archives_with_repair_calls_borg_with_repair_parameter(): ) flexmock(module).should_receive('make_check_flags').and_return(()) flexmock(module).should_receive('execute_command').never() + flexmock(module.feature).should_receive('available').and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'check', '--repair', 'repo'), @@ -369,6 +371,7 @@ def test_check_archives_calls_borg_with_parameters(checks): flexmock(module).should_receive('make_check_flags').with_args( checks, check_last, module.DEFAULT_PREFIX ).and_return(()) + flexmock(module.feature).should_receive('available').and_return(False) insert_execute_command_mock(('borg', 'check', 'repo')) flexmock(module).should_receive('make_check_time_path') flexmock(module).should_receive('write_check_time') @@ -382,6 +385,32 @@ def test_check_archives_calls_borg_with_parameters(checks): ) +def test_check_archives_with_borg_features_calls_borg_with_repo_flag(): + checks = ('repository',) + check_last = flexmock() + consistency_config = {'check_last': check_last} + flexmock(module).should_receive('parse_checks') + flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks) + flexmock(module.rinfo).should_receive('display_repository_info').and_return( + '{"repository": {"id": "repo"}}' + ) + flexmock(module).should_receive('make_check_flags').with_args( + checks, check_last, module.DEFAULT_PREFIX + ).and_return(()) + flexmock(module.feature).should_receive('available').and_return(True) + insert_execute_command_mock(('borg', 'check', '--repo', 'repo')) + flexmock(module).should_receive('make_check_time_path') + flexmock(module).should_receive('write_check_time') + + module.check_archives( + repository='repo', + location_config={}, + storage_config={}, + consistency_config=consistency_config, + local_borg_version='1.2.3', + ) + + def test_check_archives_with_json_error_raises(): checks = ('archives',) check_last = flexmock() @@ -430,6 +459,7 @@ def test_check_archives_with_extract_check_calls_extract_only(): '{"repository": {"id": "repo"}}' ) flexmock(module).should_receive('make_check_flags').never() + flexmock(module.feature).should_receive('available').and_return(False) flexmock(module.extract).should_receive('extract_last_archive_dry_run').once() flexmock(module).should_receive('write_check_time') insert_execute_command_never() @@ -452,6 +482,7 @@ def test_check_archives_with_log_info_calls_borg_with_info_parameter(): '{"repository": {"id": "repo"}}' ) flexmock(module).should_receive('make_check_flags').and_return(()) + flexmock(module.feature).should_receive('available').and_return(False) insert_logging_mock(logging.INFO) insert_execute_command_mock(('borg', 'check', '--info', 'repo')) flexmock(module).should_receive('make_check_time_path') @@ -475,6 +506,7 @@ def test_check_archives_with_log_debug_calls_borg_with_debug_parameter(): '{"repository": {"id": "repo"}}' ) flexmock(module).should_receive('make_check_flags').and_return(()) + flexmock(module.feature).should_receive('available').and_return(False) insert_logging_mock(logging.DEBUG) insert_execute_command_mock(('borg', 'check', '--debug', '--show-rc', 'repo')) flexmock(module).should_receive('make_check_time_path') @@ -519,6 +551,7 @@ def test_check_archives_with_local_path_calls_borg_via_local_path(): flexmock(module).should_receive('make_check_flags').with_args( checks, check_last, module.DEFAULT_PREFIX ).and_return(()) + flexmock(module.feature).should_receive('available').and_return(False) insert_execute_command_mock(('borg1', 'check', 'repo')) flexmock(module).should_receive('make_check_time_path') flexmock(module).should_receive('write_check_time') @@ -545,6 +578,7 @@ def test_check_archives_with_remote_path_calls_borg_with_remote_path_parameters( flexmock(module).should_receive('make_check_flags').with_args( checks, check_last, module.DEFAULT_PREFIX ).and_return(()) + flexmock(module.feature).should_receive('available').and_return(False) insert_execute_command_mock(('borg', 'check', '--remote-path', 'borg1', 'repo')) flexmock(module).should_receive('make_check_time_path') flexmock(module).should_receive('write_check_time') @@ -571,6 +605,7 @@ def test_check_archives_with_lock_wait_calls_borg_with_lock_wait_parameters(): flexmock(module).should_receive('make_check_flags').with_args( checks, check_last, module.DEFAULT_PREFIX ).and_return(()) + flexmock(module.feature).should_receive('available').and_return(False) insert_execute_command_mock(('borg', 'check', '--lock-wait', '5', 'repo')) flexmock(module).should_receive('make_check_time_path') flexmock(module).should_receive('write_check_time') @@ -597,6 +632,7 @@ def test_check_archives_with_retention_prefix(): flexmock(module).should_receive('make_check_flags').with_args( checks, check_last, prefix ).and_return(()) + flexmock(module.feature).should_receive('available').and_return(False) insert_execute_command_mock(('borg', 'check', 'repo')) flexmock(module).should_receive('make_check_time_path') flexmock(module).should_receive('write_check_time') @@ -619,6 +655,7 @@ def test_check_archives_with_extra_borg_options_calls_borg_with_extra_options(): '{"repository": {"id": "repo"}}' ) flexmock(module).should_receive('make_check_flags').and_return(()) + flexmock(module.feature).should_receive('available').and_return(False) insert_execute_command_mock(('borg', 'check', '--extra', '--options', 'repo')) flexmock(module).should_receive('make_check_time_path') flexmock(module).should_receive('write_check_time') From c7176bd00a379c53c3ef46cae11756a51d442838 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Fri, 12 Aug 2022 23:06:56 -0700 Subject: [PATCH 11/38] Add rinfo action for Borg 2 support (#557). --- borgmatic/commands/arguments.py | 33 +++++++++++++++----- borgmatic/commands/borgmatic.py | 20 +++++++++++- tests/integration/commands/test_arguments.py | 14 +++++++++ tests/unit/commands/test_borgmatic.py | 25 +++++++++++++++ 4 files changed, 83 insertions(+), 9 deletions(-) diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index 052240640..f4144531e 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -15,6 +15,7 @@ SUBPARSER_ALIASES = { 'umount': ['--umount', '-u'], 'restore': ['--restore', '-r'], 'list': ['--list', '-l'], + 'rinfo': [], 'info': ['--info', '-i'], 'borg': [], } @@ -613,17 +614,34 @@ def make_parsers(): ) list_group.add_argument('-h', '--help', action='help', help='Show this help message and exit') + rinfo_parser = subparsers.add_parser( + 'rinfo', + aliases=SUBPARSER_ALIASES['rinfo'], + help='Show repository summary information such as disk space used', + description='Show repository summary information such as disk space used', + add_help=False, + ) + rinfo_group = rinfo_parser.add_argument_group('rinfo arguments') + rinfo_group.add_argument( + '--repository', + help='Path of repository to show info for, defaults to the configured repository if there is only one', + ) + rinfo_group.add_argument( + '--json', dest='json', default=False, action='store_true', help='Output results as JSON' + ) + rinfo_group.add_argument('-h', '--help', action='help', help='Show this help message and exit') + info_parser = subparsers.add_parser( 'info', aliases=SUBPARSER_ALIASES['info'], - help='Display summary information on archives', - description='Display summary information on archives', + help='Show archive summary information such as disk space used', + description='Show archive summary information such as disk space used', add_help=False, ) info_group = info_parser.add_argument_group('info arguments') info_group.add_argument( '--repository', - help='Path of repository to show info for, defaults to the configured repository if there is only one', + help='Path of repository containing archive to show info for, defaults to the configured repository if there is only one', ) info_group.add_argument('--archive', help='Name of archive to show info for (or "latest")') info_group.add_argument( @@ -697,12 +715,11 @@ def parse_arguments(*unparsed_arguments): raise ValueError('The rcreate/init action cannot be used with the --dry-run flag') if ( - 'list' in arguments - and 'info' in arguments - and arguments['list'].json - and arguments['info'].json + ('list' in arguments and 'rinfo' in arguments and arguments['list'].json) + or ('list' in arguments and 'info' in arguments and arguments['list'].json) + or ('rinfo' in arguments and 'info' in arguments and arguments['rinfo'].json) ): - raise ValueError('With the --json flag, list and info actions cannot be used together') + raise ValueError('With the --json flag, multiple actions cannot be used together') if 'info' in arguments and arguments['info'].archive and arguments['info'].glob_archives: raise ValueError( diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index 494f41f85..d9901a2c8 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -24,6 +24,7 @@ from borgmatic.borg import list as borg_list from borgmatic.borg import mount as borg_mount from borgmatic.borg import prune as borg_prune from borgmatic.borg import rcreate as borg_rcreate +from borgmatic.borg import rinfo as borg_rinfo from borgmatic.borg import umount as borg_umount from borgmatic.borg import version as borg_version from borgmatic.commands.arguments import parse_arguments @@ -613,13 +614,30 @@ def run_actions( ) if json_output: # pragma: nocover yield json.loads(json_output) + if 'rinfo' in arguments: + if arguments['rinfo'].repository is None or validate.repositories_match( + repository, arguments['rinfo'].repository + ): + rinfo_arguments = copy.copy(arguments['rinfo']) + if not rinfo_arguments.json: # pragma: nocover + logger.warning('{}: Displaying repository summary information'.format(repository)) + json_output = borg_rinfo.display_repository_info( + repository, + storage, + local_borg_version, + rinfo_arguments=rinfo_arguments, + local_path=local_path, + remote_path=remote_path, + ) + if json_output: # pragma: nocover + yield json.loads(json_output) if 'info' in arguments: if arguments['info'].repository is None or validate.repositories_match( repository, arguments['info'].repository ): info_arguments = copy.copy(arguments['info']) if not info_arguments.json: # pragma: nocover - logger.warning('{}: Displaying summary info for archives'.format(repository)) + logger.warning('{}: Displaying archive summary information'.format(repository)) info_arguments.archive = borg_list.resolve_archive_name( repository, info_arguments.archive, storage, local_path, remote_path ) diff --git a/tests/integration/commands/test_arguments.py b/tests/integration/commands/test_arguments.py index 5eb51013d..a9e4f5cc8 100644 --- a/tests/integration/commands/test_arguments.py +++ b/tests/integration/commands/test_arguments.py @@ -496,6 +496,20 @@ def test_parse_arguments_disallows_json_with_both_list_and_info(): module.parse_arguments('list', 'info', '--json') +def test_parse_arguments_disallows_json_with_both_list_and_rinfo(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(ValueError): + module.parse_arguments('list', 'rinfo', '--json') + + +def test_parse_arguments_disallows_json_with_both_rinfo_and_info(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(ValueError): + module.parse_arguments('rinfo', 'info', '--json') + + def test_parse_arguments_disallows_info_with_both_archive_and_glob_archives(): flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) diff --git a/tests/unit/commands/test_borgmatic.py b/tests/unit/commands/test_borgmatic.py index 57ce1b4f6..e17c20ef9 100644 --- a/tests/unit/commands/test_borgmatic.py +++ b/tests/unit/commands/test_borgmatic.py @@ -597,6 +597,31 @@ def test_run_actions_does_not_raise_for_list_action(): ) +def test_run_actions_does_not_raise_for_rinfo_action(): + flexmock(module.validate).should_receive('repositories_match').and_return(True) + flexmock(module.borg_rinfo).should_receive('display_repository_info') + arguments = { + 'global': flexmock(monitoring_verbosity=1, dry_run=False), + 'rinfo': flexmock(repository=flexmock(), json=flexmock()), + } + + list( + module.run_actions( + arguments=arguments, + config_filename='test.yaml', + location={'repositories': ['repo']}, + storage={}, + retention={}, + consistency={}, + hooks={}, + local_path=None, + remote_path=None, + local_borg_version=None, + repository_path='repo', + ) + ) + + def test_run_actions_does_not_raise_for_info_action(): flexmock(module.validate).should_receive('repositories_match').and_return(True) flexmock(module.borg_list).should_receive('resolve_archive_name').and_return(flexmock()) From 2898e63166aecbdcfa8b26a092fbaabda7e4cd6a Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Fri, 12 Aug 2022 23:54:13 -0700 Subject: [PATCH 12/38] Update create action for Borg 2 support (#557). --- NEWS | 3 +- borgmatic/borg/create.py | 6 +- tests/unit/borg/test_create.py | 243 +++++++++++++++++++++++++++------ 3 files changed, 210 insertions(+), 42 deletions(-) diff --git a/NEWS b/NEWS index 5615aead5..0ad278770 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,6 @@ 2.0.0.dev0 - * #557: Support for Borg 2 while still working with Borg 1. + * #557: Support for Borg 2 while still working with Borg 1. If you install Borg 2, you'll need to + manually "borg transfer" or "borgmatic transfer" any existing Borg 1 repositories before use. * #565: Fix handling of "repository" and "data" consistency checks to prevent invalid Borg flags. * #566: Modify "mount" and "extract" actions to require the "--repository" flag when multiple repositories are configured. diff --git a/borgmatic/borg/create.py b/borgmatic/borg/create.py index 1a7ae4004..c5a81c535 100644 --- a/borgmatic/borg/create.py +++ b/borgmatic/borg/create.py @@ -299,9 +299,9 @@ def create_archive( + (('--json',) if json else ()) + (tuple(extra_borg_options.split(' ')) if extra_borg_options else ()) + ( - '{repository}::{archive_name_format}'.format( - repository=repository, archive_name_format=archive_name_format - ), + ('--repo', repository, archive_name_format) + if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version) + else (f'{repository}::{archive_name_format}',) ) + sources ) diff --git a/tests/unit/borg/test_create.py b/tests/unit/borg/test_create.py index c51bd34fa..5b055461f 100644 --- a/tests/unit/borg/test_create.py +++ b/tests/unit/borg/test_create.py @@ -277,7 +277,7 @@ def test_borgmatic_source_directories_defaults_when_directory_not_given(): DEFAULT_ARCHIVE_NAME = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}' -ARCHIVE_WITH_PATHS = ('repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'bar') +REPO_ARCHIVE_WITH_PATHS = (f'repo::{DEFAULT_ARCHIVE_NAME}', 'foo', 'bar') def test_create_archive_calls_borg_with_parameters(): @@ -292,9 +292,50 @@ def test_create_archive_calls_borg_with_parameters(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create') + ARCHIVE_WITH_PATHS, + ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS, + output_log_level=logging.INFO, + output_file=None, + borg_local_path='borg', + working_directory=None, + extra_environment=None, + ) + + module.create_archive( + dry_run=False, + repository='repo', + location_config={ + 'source_directories': ['foo', 'bar'], + 'repositories': ['repo'], + 'exclude_patterns': None, + }, + storage_config={}, + local_borg_version='1.2.3', + ) + + +def test_create_archive_with_borg_features_calls_borg_with_repo_flag(): + flexmock(module).should_receive('borgmatic_source_directories').and_return([]) + flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) + flexmock(module).should_receive('map_directories_to_devices').and_return({}) + flexmock(module).should_receive('expand_directories').and_return(()) + flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError) + flexmock(module).should_receive('expand_home_directories').and_return(()) + flexmock(module).should_receive('write_pattern_file').and_return(None) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module).should_receive('ensure_files_readable') + flexmock(module).should_receive('make_pattern_flags').and_return(()) + flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(True) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'create', '--repo', 'repo', DEFAULT_ARCHIVE_NAME, 'foo', 'bar'), output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -327,10 +368,13 @@ def test_create_archive_calls_borg_with_environment(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) environment = {'BORG_THINGY': 'YUP'} flexmock(module.environment).should_receive('make_environment').and_return(environment) flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create') + ARCHIVE_WITH_PATHS, + ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -366,9 +410,12 @@ def test_create_archive_with_patterns_calls_borg_with_patterns(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(pattern_flags) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create') + pattern_flags + ARCHIVE_WITH_PATHS, + ('borg', 'create') + pattern_flags + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -404,9 +451,12 @@ def test_create_archive_with_exclude_patterns_calls_borg_with_excludes(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(exclude_flags) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create') + exclude_flags + ARCHIVE_WITH_PATHS, + ('borg', 'create') + exclude_flags + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -439,9 +489,12 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--info') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--info') + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -475,9 +528,12 @@ def test_create_archive_with_log_info_and_json_suppresses_most_borg_output(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--json') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS, output_log_level=None, output_file=None, borg_local_path='borg', @@ -512,9 +568,12 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--debug', '--show-rc') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--debug', '--show-rc') + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -548,9 +607,12 @@ def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--json') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS, output_log_level=None, output_file=None, borg_local_path='borg', @@ -585,9 +647,12 @@ def test_create_archive_with_dry_run_calls_borg_with_dry_run_parameter(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--dry-run') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--dry-run') + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -622,9 +687,12 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats_paramete flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--info', '--dry-run') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--info', '--dry-run') + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -659,9 +727,12 @@ def test_create_archive_with_checkpoint_interval_calls_borg_with_checkpoint_inte flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--checkpoint-interval', '600') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--checkpoint-interval', '600') + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -694,9 +765,12 @@ def test_create_archive_with_chunker_params_calls_borg_with_chunker_params_param flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--chunker-params', '1,2,3,4') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--chunker-params', '1,2,3,4') + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -729,9 +803,12 @@ def test_create_archive_with_compression_calls_borg_with_compression_parameters( flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--compression', 'rle') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--compression', 'rle') + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -769,9 +846,12 @@ def test_create_archive_with_remote_rate_limit_calls_borg_with_upload_ratelimit_ flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', option_flag, '100') + ARCHIVE_WITH_PATHS, + ('borg', 'create', option_flag, '100') + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -806,9 +886,12 @@ def test_create_archive_with_working_directory_calls_borg_with_working_directory flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create') + ARCHIVE_WITH_PATHS, + ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -842,9 +925,12 @@ def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_par flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--one-file-system') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--one-file-system') + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -883,9 +969,12 @@ def test_create_archive_with_numeric_owner_calls_borg_with_numeric_ids_parameter flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', option_flag) + ARCHIVE_WITH_PATHS, + ('borg', 'create', option_flag) + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -919,9 +1008,12 @@ def test_create_archive_with_read_special_calls_borg_with_read_special_parameter flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--read-special') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--read-special') + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -962,9 +1054,12 @@ def test_create_archive_with_basic_option_calls_borg_with_corresponding_paramete flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create') + ((option_flag,) if option_flag else ()) + ARCHIVE_WITH_PATHS, + ('borg', 'create') + ((option_flag,) if option_flag else ()) + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -1009,9 +1104,12 @@ def test_create_archive_with_atime_option_calls_borg_with_corresponding_paramete flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create') + ((option_flag,) if option_flag else ()) + ARCHIVE_WITH_PATHS, + ('borg', 'create') + ((option_flag,) if option_flag else ()) + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -1056,9 +1154,12 @@ def test_create_archive_with_bsd_flags_option_calls_borg_with_corresponding_para flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create') + ((option_flag,) if option_flag else ()) + ARCHIVE_WITH_PATHS, + ('borg', 'create') + ((option_flag,) if option_flag else ()) + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -1092,9 +1193,12 @@ def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters( flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--files-cache', 'ctime,size') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--files-cache', 'ctime,size') + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -1128,9 +1232,12 @@ def test_create_archive_with_local_path_calls_borg_via_local_path(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg1', 'create') + ARCHIVE_WITH_PATHS, + ('borg1', 'create') + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg1', @@ -1164,9 +1271,12 @@ def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters( flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--remote-path', 'borg1') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--remote-path', 'borg1') + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -1200,9 +1310,12 @@ def test_create_archive_with_umask_calls_borg_with_umask_parameters(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--umask', '740') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--umask', '740') + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -1235,9 +1348,12 @@ def test_create_archive_with_lock_wait_calls_borg_with_lock_wait_parameters(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--lock-wait', '5') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--lock-wait', '5') + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -1270,9 +1386,12 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_warning_o flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--stats') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--stats') + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.WARNING, output_file=None, borg_local_path='borg', @@ -1306,9 +1425,12 @@ def test_create_archive_with_stats_and_log_info_calls_borg_with_stats_parameter_ flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--info', '--stats') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--info', '--stats') + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -1343,9 +1465,12 @@ def test_create_archive_with_files_calls_borg_with_list_parameter_and_warning_ou flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--list', '--filter', 'AME-') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--list', '--filter', 'AME-') + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.WARNING, output_file=None, borg_local_path='borg', @@ -1379,9 +1504,12 @@ def test_create_archive_with_files_and_log_info_calls_borg_with_list_parameter_a flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--list', '--filter', 'AME-', '--info') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--list', '--filter', 'AME-', '--info') + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -1416,9 +1544,12 @@ def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_para flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--info', '--progress') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--info', '--progress') + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=module.DO_NOT_CAPTURE, borg_local_path='borg', @@ -1453,9 +1584,12 @@ def test_create_archive_with_progress_calls_borg_with_progress_parameter(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--progress') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--progress') + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=module.DO_NOT_CAPTURE, borg_local_path='borg', @@ -1490,10 +1624,13 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command_with_processes').with_args( ('borg', 'create', '--one-file-system', '--read-special', '--progress') - + ARCHIVE_WITH_PATHS, + + REPO_ARCHIVE_WITH_PATHS, processes=processes, output_log_level=logging.INFO, output_file=module.DO_NOT_CAPTURE, @@ -1529,9 +1666,12 @@ def test_create_archive_with_json_calls_borg_with_json_parameter(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--json') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS, output_log_level=None, output_file=None, borg_local_path='borg', @@ -1567,9 +1707,12 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_parameter() flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--json') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS, output_log_level=None, output_file=None, borg_local_path='borg', @@ -1606,6 +1749,9 @@ def test_create_archive_with_source_directories_glob_expands(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'food'), @@ -1642,6 +1788,9 @@ def test_create_archive_with_non_matching_source_directories_glob_passes_through flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo*'), @@ -1678,6 +1827,9 @@ def test_create_archive_with_glob_calls_borg_with_expanded_directories(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'food'), @@ -1713,6 +1865,9 @@ def test_create_archive_with_archive_name_format_calls_borg_with_archive_name(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', 'repo::ARCHIVE_NAME', 'foo', 'bar'), @@ -1748,6 +1903,9 @@ def test_create_archive_with_archive_name_format_accepts_borg_placeholders(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', 'repo::Documents_{hostname}-{now}', 'foo', 'bar'), @@ -1783,6 +1941,9 @@ def test_create_archive_with_repository_accepts_borg_placeholders(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '{fqdn}::Documents_{hostname}-{now}', 'foo', 'bar'), @@ -1818,9 +1979,12 @@ def test_create_archive_with_extra_borg_options_calls_borg_with_extra_options(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--extra', '--options') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--extra', '--options') + REPO_ARCHIVE_WITH_PATHS, output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -1854,9 +2018,12 @@ def test_create_archive_with_stream_processes_calls_borg_with_processes(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' + ).and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command_with_processes').with_args( - ('borg', 'create', '--one-file-system', '--read-special') + ARCHIVE_WITH_PATHS, + ('borg', 'create', '--one-file-system', '--read-special') + REPO_ARCHIVE_WITH_PATHS, processes=processes, output_log_level=logging.INFO, output_file=None, From 4a55749bd2de5086672fecdb4978fce57037c0ce Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Sat, 13 Aug 2022 17:26:51 -0700 Subject: [PATCH 13/38] Update prune action for Borg 2 support (#557). --- NEWS | 4 +- borgmatic/borg/prune.py | 20 ++++--- borgmatic/commands/borgmatic.py | 1 + tests/unit/borg/test_prune.py | 103 ++++++++++++++++++++++++-------- 4 files changed, 95 insertions(+), 33 deletions(-) diff --git a/NEWS b/NEWS index 0ad278770..2229bbb99 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,8 @@ 2.0.0.dev0 * #557: Support for Borg 2 while still working with Borg 1. If you install Borg 2, you'll need to - manually "borg transfer" or "borgmatic transfer" any existing Borg 1 repositories before use. + manually "borg transfer" or "borgmatic transfer" any existing Borg 1 repositories before use. See + the Borg 2.0 changelog summary for more information about Borg 2: + https://www.borgbackup.org/releases/borg-2.0.html * #565: Fix handling of "repository" and "data" consistency checks to prevent invalid Borg flags. * #566: Modify "mount" and "extract" actions to require the "--repository" flag when multiple repositories are configured. diff --git a/borgmatic/borg/prune.py b/borgmatic/borg/prune.py index 9b2f2b44d..60898a1a9 100644 --- a/borgmatic/borg/prune.py +++ b/borgmatic/borg/prune.py @@ -1,12 +1,12 @@ import logging -from borgmatic.borg import environment +from borgmatic.borg import environment, feature from borgmatic.execute import execute_command logger = logging.getLogger(__name__) -def _make_prune_flags(retention_config): +def make_prune_flags(retention_config): ''' Given a retention config dict mapping from option name to value, tranform it into an iterable of command-line name-value flag pairs. @@ -23,11 +23,9 @@ def _make_prune_flags(retention_config): ) ''' config = retention_config.copy() - - if 'prefix' not in config: - config['prefix'] = '{hostname}-' - elif not config['prefix']: - config.pop('prefix') + prefix = config.pop('prefix', '{hostname}-') + if prefix: + config['glob_archives'] = f'{prefix}*' return ( ('--' + option_name.replace('_', '-'), str(value)) for option_name, value in config.items() @@ -39,6 +37,7 @@ def prune_archives( repository, storage_config, retention_config, + local_borg_version, local_path='borg', remote_path=None, stats=False, @@ -55,7 +54,7 @@ def prune_archives( full_command = ( (local_path, 'prune') - + tuple(element for pair in _make_prune_flags(retention_config) for element in pair) + + tuple(element for pair in make_prune_flags(retention_config) for element in pair) + (('--remote-path', remote_path) if remote_path else ()) + (('--umask', str(umask)) if umask else ()) + (('--lock-wait', str(lock_wait)) if lock_wait else ()) @@ -65,6 +64,11 @@ def prune_archives( + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ()) + (('--dry-run',) if dry_run else ()) + (tuple(extra_borg_options.split(' ')) if extra_borg_options else ()) + + ( + ('--repo',) + if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version) + else () + ) + (repository,) ) diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index d9901a2c8..a4e5714c2 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -277,6 +277,7 @@ def run_actions( repository, storage, retention, + local_borg_version, local_path=local_path, remote_path=remote_path, stats=arguments['prune'].stats, diff --git a/tests/unit/borg/test_prune.py b/tests/unit/borg/test_prune.py index f4f34edd2..1ca794d70 100644 --- a/tests/unit/borg/test_prune.py +++ b/tests/unit/borg/test_prune.py @@ -21,20 +21,20 @@ def insert_execute_command_mock(prune_command, output_log_level): BASE_PRUNE_FLAGS = (('--keep-daily', '1'), ('--keep-weekly', '2'), ('--keep-monthly', '3')) -def test_make_prune_flags_returns_flags_from_config_plus_default_prefix(): +def test_make_prune_flags_returns_flags_from_config_plus_default_prefix_glob(): retention_config = OrderedDict((('keep_daily', 1), ('keep_weekly', 2), ('keep_monthly', 3))) - result = module._make_prune_flags(retention_config) + result = module.make_prune_flags(retention_config) - assert tuple(result) == BASE_PRUNE_FLAGS + (('--prefix', '{hostname}-'),) + assert tuple(result) == BASE_PRUNE_FLAGS + (('--glob-archives', '{hostname}-*'),) def test_make_prune_flags_accepts_prefix_with_placeholders(): retention_config = OrderedDict((('keep_daily', 1), ('prefix', 'Documents_{hostname}-{now}'))) - result = module._make_prune_flags(retention_config) + result = module.make_prune_flags(retention_config) - expected = (('--keep-daily', '1'), ('--prefix', 'Documents_{hostname}-{now}')) + expected = (('--keep-daily', '1'), ('--glob-archives', 'Documents_{hostname}-{now}*')) assert tuple(result) == expected @@ -42,7 +42,7 @@ def test_make_prune_flags_accepts_prefix_with_placeholders(): def test_make_prune_flags_treats_empty_prefix_as_no_prefix(): retention_config = OrderedDict((('keep_daily', 1), ('prefix', ''))) - result = module._make_prune_flags(retention_config) + result = module.make_prune_flags(retention_config) expected = (('--keep-daily', '1'),) @@ -52,7 +52,7 @@ def test_make_prune_flags_treats_empty_prefix_as_no_prefix(): def test_make_prune_flags_treats_none_prefix_as_no_prefix(): retention_config = OrderedDict((('keep_daily', 1), ('prefix', None))) - result = module._make_prune_flags(retention_config) + result = module.make_prune_flags(retention_config) expected = (('--keep-daily', '1'),) @@ -64,59 +64,97 @@ PRUNE_COMMAND = ('borg', 'prune', '--keep-daily', '1', '--keep-weekly', '2', '-- def test_prune_archives_calls_borg_with_parameters(): retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.feature).should_receive('available').and_return(False) insert_execute_command_mock(PRUNE_COMMAND + ('repo',), logging.INFO) module.prune_archives( - dry_run=False, repository='repo', storage_config={}, retention_config=retention_config + dry_run=False, + repository='repo', + storage_config={}, + retention_config=retention_config, + local_borg_version='1.2.3', + ) + + +def test_prune_archives_with_borg_features_calls_borg_with_repo_flag(): + retention_config = flexmock() + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( + BASE_PRUNE_FLAGS + ) + flexmock(module.feature).should_receive('available').and_return(True) + insert_execute_command_mock(PRUNE_COMMAND + ('--repo', 'repo'), logging.INFO) + + module.prune_archives( + dry_run=False, + repository='repo', + storage_config={}, + retention_config=retention_config, + local_borg_version='1.2.3', ) def test_prune_archives_with_log_info_calls_borg_with_info_parameter(): retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.feature).should_receive('available').and_return(False) insert_execute_command_mock(PRUNE_COMMAND + ('--info', 'repo'), logging.INFO) insert_logging_mock(logging.INFO) module.prune_archives( - repository='repo', storage_config={}, dry_run=False, retention_config=retention_config + repository='repo', + storage_config={}, + dry_run=False, + retention_config=retention_config, + local_borg_version='1.2.3', ) def test_prune_archives_with_log_debug_calls_borg_with_debug_parameter(): retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.feature).should_receive('available').and_return(False) insert_execute_command_mock(PRUNE_COMMAND + ('--debug', '--show-rc', 'repo'), logging.INFO) insert_logging_mock(logging.DEBUG) module.prune_archives( - repository='repo', storage_config={}, dry_run=False, retention_config=retention_config + repository='repo', + storage_config={}, + dry_run=False, + retention_config=retention_config, + local_borg_version='1.2.3', ) def test_prune_archives_with_dry_run_calls_borg_with_dry_run_parameter(): retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.feature).should_receive('available').and_return(False) insert_execute_command_mock(PRUNE_COMMAND + ('--dry-run', 'repo'), logging.INFO) module.prune_archives( - repository='repo', storage_config={}, dry_run=True, retention_config=retention_config + repository='repo', + storage_config={}, + dry_run=True, + retention_config=retention_config, + local_borg_version='1.2.3', ) def test_prune_archives_with_local_path_calls_borg_via_local_path(): retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.feature).should_receive('available').and_return(False) insert_execute_command_mock(('borg1',) + PRUNE_COMMAND[1:] + ('repo',), logging.INFO) module.prune_archives( @@ -124,15 +162,17 @@ def test_prune_archives_with_local_path_calls_borg_via_local_path(): repository='repo', storage_config={}, retention_config=retention_config, + local_borg_version='1.2.3', local_path='borg1', ) def test_prune_archives_with_remote_path_calls_borg_with_remote_path_parameters(): retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.feature).should_receive('available').and_return(False) insert_execute_command_mock(PRUNE_COMMAND + ('--remote-path', 'borg1', 'repo'), logging.INFO) module.prune_archives( @@ -140,15 +180,17 @@ def test_prune_archives_with_remote_path_calls_borg_with_remote_path_parameters( repository='repo', storage_config={}, retention_config=retention_config, + local_borg_version='1.2.3', remote_path='borg1', ) def test_prune_archives_with_stats_calls_borg_with_stats_parameter_and_warning_output_log_level(): retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.feature).should_receive('available').and_return(False) insert_execute_command_mock(PRUNE_COMMAND + ('--stats', 'repo'), logging.WARNING) module.prune_archives( @@ -156,15 +198,17 @@ def test_prune_archives_with_stats_calls_borg_with_stats_parameter_and_warning_o repository='repo', storage_config={}, retention_config=retention_config, + local_borg_version='1.2.3', stats=True, ) def test_prune_archives_with_stats_and_log_info_calls_borg_with_stats_parameter_and_info_output_log_level(): retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.feature).should_receive('available').and_return(False) insert_logging_mock(logging.INFO) insert_execute_command_mock(PRUNE_COMMAND + ('--stats', '--info', 'repo'), logging.INFO) @@ -173,15 +217,17 @@ def test_prune_archives_with_stats_and_log_info_calls_borg_with_stats_parameter_ repository='repo', storage_config={}, retention_config=retention_config, + local_borg_version='1.2.3', stats=True, ) def test_prune_archives_with_files_calls_borg_with_list_parameter_and_warning_output_log_level(): retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.feature).should_receive('available').and_return(False) insert_execute_command_mock(PRUNE_COMMAND + ('--list', 'repo'), logging.WARNING) module.prune_archives( @@ -189,15 +235,17 @@ def test_prune_archives_with_files_calls_borg_with_list_parameter_and_warning_ou repository='repo', storage_config={}, retention_config=retention_config, + local_borg_version='1.2.3', files=True, ) def test_prune_archives_with_files_and_log_info_calls_borg_with_list_parameter_and_info_output_log_level(): retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.feature).should_receive('available').and_return(False) insert_logging_mock(logging.INFO) insert_execute_command_mock(PRUNE_COMMAND + ('--info', '--list', 'repo'), logging.INFO) @@ -206,6 +254,7 @@ def test_prune_archives_with_files_and_log_info_calls_borg_with_list_parameter_a repository='repo', storage_config={}, retention_config=retention_config, + local_borg_version='1.2.3', files=True, ) @@ -213,9 +262,10 @@ def test_prune_archives_with_files_and_log_info_calls_borg_with_list_parameter_a def test_prune_archives_with_umask_calls_borg_with_umask_parameters(): storage_config = {'umask': '077'} retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.feature).should_receive('available').and_return(False) insert_execute_command_mock(PRUNE_COMMAND + ('--umask', '077', 'repo'), logging.INFO) module.prune_archives( @@ -223,15 +273,17 @@ def test_prune_archives_with_umask_calls_borg_with_umask_parameters(): repository='repo', storage_config=storage_config, retention_config=retention_config, + local_borg_version='1.2.3', ) def test_prune_archives_with_lock_wait_calls_borg_with_lock_wait_parameters(): storage_config = {'lock_wait': 5} retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.feature).should_receive('available').and_return(False) insert_execute_command_mock(PRUNE_COMMAND + ('--lock-wait', '5', 'repo'), logging.INFO) module.prune_archives( @@ -239,14 +291,16 @@ def test_prune_archives_with_lock_wait_calls_borg_with_lock_wait_parameters(): repository='repo', storage_config=storage_config, retention_config=retention_config, + local_borg_version='1.2.3', ) def test_prune_archives_with_extra_borg_options_calls_borg_with_extra_options(): retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.feature).should_receive('available').and_return(False) insert_execute_command_mock(PRUNE_COMMAND + ('--extra', '--options', 'repo'), logging.INFO) module.prune_archives( @@ -254,4 +308,5 @@ def test_prune_archives_with_extra_borg_options_calls_borg_with_extra_options(): repository='repo', storage_config={'extra_borg_options': {'prune': '--extra --options'}}, retention_config=retention_config, + local_borg_version='1.2.3', ) From 94321aec7a92ecd33f4de9f3bb1f9af7752c2a8a Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Sat, 13 Aug 2022 22:07:15 -0700 Subject: [PATCH 14/38] Update compact action for Borg 2 support (#557). --- NEWS | 2 +- borgmatic/borg/check.py | 11 ++---- borgmatic/borg/compact.py | 9 ++--- borgmatic/borg/flags.py | 14 ++++++++ borgmatic/borg/prune.py | 9 ++--- borgmatic/borg/rcreate.py | 9 ++--- borgmatic/borg/rinfo.py | 14 +++----- borgmatic/commands/borgmatic.py | 1 + setup.py | 2 +- tests/unit/borg/test_check.py | 48 ++++++------------------- tests/unit/borg/test_compact.py | 62 +++++++++++++++++++++++++++------ tests/unit/borg/test_flags.py | 15 ++++++++ tests/unit/borg/test_prune.py | 43 +++++++---------------- tests/unit/borg/test_rcreate.py | 20 +++++------ tests/unit/borg/test_rinfo.py | 10 ++++++ 15 files changed, 142 insertions(+), 127 deletions(-) diff --git a/NEWS b/NEWS index 2229bbb99..a996306c3 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,4 @@ -2.0.0.dev0 +1.7.0.dev0 * #557: Support for Borg 2 while still working with Borg 1. If you install Borg 2, you'll need to manually "borg transfer" or "borgmatic transfer" any existing Borg 1 repositories before use. See the Borg 2.0 changelog summary for more information about Borg 2: diff --git a/borgmatic/borg/check.py b/borgmatic/borg/check.py index 0cdf90f62..917967413 100644 --- a/borgmatic/borg/check.py +++ b/borgmatic/borg/check.py @@ -5,7 +5,7 @@ import logging import os import pathlib -from borgmatic.borg import environment, extract, feature, rinfo, state +from borgmatic.borg import environment, extract, flags, rinfo, state from borgmatic.execute import DO_NOT_CAPTURE, execute_command DEFAULT_CHECKS = ( @@ -304,14 +304,7 @@ def check_archives( + verbosity_flags + (('--progress',) if progress else ()) + (tuple(extra_borg_options.split(' ')) if extra_borg_options else ()) - + ( - ('--repo',) - if feature.available( - feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version - ) - else () - ) - + (repository,) + + flags.make_repository_flags(repository, local_borg_version) ) borg_environment = environment.make_environment(storage_config) diff --git a/borgmatic/borg/compact.py b/borgmatic/borg/compact.py index 17f0f1a64..c9770e093 100644 --- a/borgmatic/borg/compact.py +++ b/borgmatic/borg/compact.py @@ -1,6 +1,6 @@ import logging -from borgmatic.borg import environment +from borgmatic.borg import environment, flags from borgmatic.execute import execute_command logger = logging.getLogger(__name__) @@ -10,6 +10,7 @@ def compact_segments( dry_run, repository, storage_config, + local_borg_version, local_path='borg', remote_path=None, progress=False, @@ -17,8 +18,8 @@ def compact_segments( threshold=None, ): ''' - Given dry-run flag, a local or remote repository path, and a storage config dict, compact Borg - segments in a repository. + Given dry-run flag, a local or remote repository path, a storage config dict, and the local + Borg version, compact the segments in a repository. ''' umask = storage_config.get('umask', None) lock_wait = storage_config.get('lock_wait', None) @@ -35,7 +36,7 @@ def compact_segments( + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ()) + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ()) + (tuple(extra_borg_options.split(' ')) if extra_borg_options else ()) - + (repository,) + + flags.make_repository_flags(repository, local_borg_version) ) if not dry_run: diff --git a/borgmatic/borg/flags.py b/borgmatic/borg/flags.py index 0665607a2..d9e26d117 100644 --- a/borgmatic/borg/flags.py +++ b/borgmatic/borg/flags.py @@ -1,5 +1,7 @@ import itertools +from borgmatic.borg import feature + def make_flags(name, value): ''' @@ -29,3 +31,15 @@ def make_flags_from_arguments(arguments, excludes=()): if name not in excludes and not name.startswith('_') ) ) + + +def make_repository_flags(repository, local_borg_version): + ''' + Given the path of a Borg repository and the local Borg version, return Borg-version-appropriate + command-line flags (as a tuple) for selecting that repository. + ''' + return ( + ('--repo',) + if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version) + else () + ) + (repository,) diff --git a/borgmatic/borg/prune.py b/borgmatic/borg/prune.py index 60898a1a9..54c428c1d 100644 --- a/borgmatic/borg/prune.py +++ b/borgmatic/borg/prune.py @@ -1,6 +1,6 @@ import logging -from borgmatic.borg import environment, feature +from borgmatic.borg import environment, flags from borgmatic.execute import execute_command logger = logging.getLogger(__name__) @@ -64,12 +64,7 @@ def prune_archives( + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ()) + (('--dry-run',) if dry_run else ()) + (tuple(extra_borg_options.split(' ')) if extra_borg_options else ()) - + ( - ('--repo',) - if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version) - else () - ) - + (repository,) + + flags.make_repository_flags(repository, local_borg_version) ) if (stats or files) and logger.getEffectiveLevel() == logging.WARNING: diff --git a/borgmatic/borg/rcreate.py b/borgmatic/borg/rcreate.py index 8d3609379..5c0be8961 100644 --- a/borgmatic/borg/rcreate.py +++ b/borgmatic/borg/rcreate.py @@ -2,7 +2,7 @@ import argparse import logging import subprocess -from borgmatic.borg import environment, feature, rinfo +from borgmatic.borg import environment, feature, flags, rinfo from borgmatic.execute import DO_NOT_CAPTURE, execute_command logger = logging.getLogger(__name__) @@ -57,12 +57,7 @@ def create_repository( + (('--debug',) if logger.isEnabledFor(logging.DEBUG) else ()) + (('--remote-path', remote_path) if remote_path else ()) + (tuple(extra_borg_options.split(' ')) if extra_borg_options else ()) - + ( - ('--repo',) - if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version) - else () - ) - + (repository,) + + flags.make_repository_flags(repository, local_borg_version) ) # Do not capture output here, so as to support interactive prompts. diff --git a/borgmatic/borg/rinfo.py b/borgmatic/borg/rinfo.py index ca771daa1..3fc8bb5cf 100644 --- a/borgmatic/borg/rinfo.py +++ b/borgmatic/borg/rinfo.py @@ -1,7 +1,6 @@ import logging -from borgmatic.borg import environment, feature -from borgmatic.borg.flags import make_flags +from borgmatic.borg import environment, feature, flags from borgmatic.execute import execute_command logger = logging.getLogger(__name__) @@ -39,15 +38,10 @@ def display_repository_info( if logger.isEnabledFor(logging.DEBUG) and not rinfo_arguments.json else () ) - + make_flags('remote-path', remote_path) - + make_flags('lock-wait', lock_wait) + + flags.make_flags('remote-path', remote_path) + + flags.make_flags('lock-wait', lock_wait) + (('--json',) if rinfo_arguments.json else ()) - + ( - ('--repo',) - if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version) - else () - ) - + (repository,) + + flags.make_repository_flags(repository, local_borg_version) ) return execute_command( diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index a4e5714c2..f806e4d35 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -305,6 +305,7 @@ def run_actions( global_arguments.dry_run, repository, storage, + local_borg_version, local_path=local_path, remote_path=remote_path, progress=arguments['compact'].progress, diff --git a/setup.py b/setup.py index e577a25af..20d101ac7 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import find_packages, setup -VERSION = '2.0.0.dev0' +VERSION = '1.7.0.dev0' setup( diff --git a/tests/unit/borg/test_check.py b/tests/unit/borg/test_check.py index 60e416cfb..60bfcefbc 100644 --- a/tests/unit/borg/test_check.py +++ b/tests/unit/borg/test_check.py @@ -301,7 +301,7 @@ def test_check_archives_with_progress_calls_borg_with_progress_parameter(): ) flexmock(module).should_receive('make_check_flags').and_return(()) flexmock(module).should_receive('execute_command').never() - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'check', '--progress', 'repo'), @@ -331,7 +331,7 @@ def test_check_archives_with_repair_calls_borg_with_repair_parameter(): ) flexmock(module).should_receive('make_check_flags').and_return(()) flexmock(module).should_receive('execute_command').never() - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'check', '--repair', 'repo'), @@ -371,7 +371,7 @@ def test_check_archives_calls_borg_with_parameters(checks): flexmock(module).should_receive('make_check_flags').with_args( checks, check_last, module.DEFAULT_PREFIX ).and_return(()) - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(('borg', 'check', 'repo')) flexmock(module).should_receive('make_check_time_path') flexmock(module).should_receive('write_check_time') @@ -385,32 +385,6 @@ def test_check_archives_calls_borg_with_parameters(checks): ) -def test_check_archives_with_borg_features_calls_borg_with_repo_flag(): - checks = ('repository',) - check_last = flexmock() - consistency_config = {'check_last': check_last} - flexmock(module).should_receive('parse_checks') - flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks) - flexmock(module.rinfo).should_receive('display_repository_info').and_return( - '{"repository": {"id": "repo"}}' - ) - flexmock(module).should_receive('make_check_flags').with_args( - checks, check_last, module.DEFAULT_PREFIX - ).and_return(()) - flexmock(module.feature).should_receive('available').and_return(True) - insert_execute_command_mock(('borg', 'check', '--repo', 'repo')) - flexmock(module).should_receive('make_check_time_path') - flexmock(module).should_receive('write_check_time') - - module.check_archives( - repository='repo', - location_config={}, - storage_config={}, - consistency_config=consistency_config, - local_borg_version='1.2.3', - ) - - def test_check_archives_with_json_error_raises(): checks = ('archives',) check_last = flexmock() @@ -459,7 +433,7 @@ def test_check_archives_with_extract_check_calls_extract_only(): '{"repository": {"id": "repo"}}' ) flexmock(module).should_receive('make_check_flags').never() - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) flexmock(module.extract).should_receive('extract_last_archive_dry_run').once() flexmock(module).should_receive('write_check_time') insert_execute_command_never() @@ -482,7 +456,7 @@ def test_check_archives_with_log_info_calls_borg_with_info_parameter(): '{"repository": {"id": "repo"}}' ) flexmock(module).should_receive('make_check_flags').and_return(()) - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_logging_mock(logging.INFO) insert_execute_command_mock(('borg', 'check', '--info', 'repo')) flexmock(module).should_receive('make_check_time_path') @@ -506,7 +480,7 @@ def test_check_archives_with_log_debug_calls_borg_with_debug_parameter(): '{"repository": {"id": "repo"}}' ) flexmock(module).should_receive('make_check_flags').and_return(()) - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_logging_mock(logging.DEBUG) insert_execute_command_mock(('borg', 'check', '--debug', '--show-rc', 'repo')) flexmock(module).should_receive('make_check_time_path') @@ -551,7 +525,7 @@ def test_check_archives_with_local_path_calls_borg_via_local_path(): flexmock(module).should_receive('make_check_flags').with_args( checks, check_last, module.DEFAULT_PREFIX ).and_return(()) - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(('borg1', 'check', 'repo')) flexmock(module).should_receive('make_check_time_path') flexmock(module).should_receive('write_check_time') @@ -578,7 +552,7 @@ def test_check_archives_with_remote_path_calls_borg_with_remote_path_parameters( flexmock(module).should_receive('make_check_flags').with_args( checks, check_last, module.DEFAULT_PREFIX ).and_return(()) - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(('borg', 'check', '--remote-path', 'borg1', 'repo')) flexmock(module).should_receive('make_check_time_path') flexmock(module).should_receive('write_check_time') @@ -605,7 +579,7 @@ def test_check_archives_with_lock_wait_calls_borg_with_lock_wait_parameters(): flexmock(module).should_receive('make_check_flags').with_args( checks, check_last, module.DEFAULT_PREFIX ).and_return(()) - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(('borg', 'check', '--lock-wait', '5', 'repo')) flexmock(module).should_receive('make_check_time_path') flexmock(module).should_receive('write_check_time') @@ -632,7 +606,7 @@ def test_check_archives_with_retention_prefix(): flexmock(module).should_receive('make_check_flags').with_args( checks, check_last, prefix ).and_return(()) - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(('borg', 'check', 'repo')) flexmock(module).should_receive('make_check_time_path') flexmock(module).should_receive('write_check_time') @@ -655,7 +629,7 @@ def test_check_archives_with_extra_borg_options_calls_borg_with_extra_options(): '{"repository": {"id": "repo"}}' ) flexmock(module).should_receive('make_check_flags').and_return(()) - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(('borg', 'check', '--extra', '--options', 'repo')) flexmock(module).should_receive('make_check_time_path') flexmock(module).should_receive('write_check_time') diff --git a/tests/unit/borg/test_compact.py b/tests/unit/borg/test_compact.py index 4c2abb7a0..36760f3c3 100644 --- a/tests/unit/borg/test_compact.py +++ b/tests/unit/borg/test_compact.py @@ -21,94 +21,134 @@ COMPACT_COMMAND = ('borg', 'compact') def test_compact_segments_calls_borg_with_parameters(): + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(COMPACT_COMMAND + ('repo',), logging.INFO) - module.compact_segments(dry_run=False, repository='repo', storage_config={}) + module.compact_segments( + dry_run=False, repository='repo', storage_config={}, local_borg_version='1.2.3' + ) def test_compact_segments_with_log_info_calls_borg_with_info_parameter(): + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(COMPACT_COMMAND + ('--info', 'repo'), logging.INFO) insert_logging_mock(logging.INFO) - module.compact_segments(repository='repo', storage_config={}, dry_run=False) + module.compact_segments( + repository='repo', storage_config={}, local_borg_version='1.2.3', dry_run=False + ) def test_compact_segments_with_log_debug_calls_borg_with_debug_parameter(): + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(COMPACT_COMMAND + ('--debug', '--show-rc', 'repo'), logging.INFO) insert_logging_mock(logging.DEBUG) - module.compact_segments(repository='repo', storage_config={}, dry_run=False) + module.compact_segments( + repository='repo', storage_config={}, local_borg_version='1.2.3', dry_run=False + ) def test_compact_segments_with_dry_run_skips_borg_call(): flexmock(module).should_receive('execute_command').never() - module.compact_segments(repository='repo', storage_config={}, dry_run=True) + module.compact_segments( + repository='repo', storage_config={}, local_borg_version='1.2.3', dry_run=True + ) def test_compact_segments_with_local_path_calls_borg_via_local_path(): + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(('borg1',) + COMPACT_COMMAND[1:] + ('repo',), logging.INFO) module.compact_segments( - dry_run=False, repository='repo', storage_config={}, local_path='borg1', + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + local_path='borg1', ) def test_compact_segments_with_remote_path_calls_borg_with_remote_path_parameters(): + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(COMPACT_COMMAND + ('--remote-path', 'borg1', 'repo'), logging.INFO) module.compact_segments( - dry_run=False, repository='repo', storage_config={}, remote_path='borg1', + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + remote_path='borg1', ) def test_compact_segments_with_progress_calls_borg_with_progress_parameter(): + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(COMPACT_COMMAND + ('--progress', 'repo'), logging.INFO) module.compact_segments( - dry_run=False, repository='repo', storage_config={}, progress=True, + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + progress=True, ) def test_compact_segments_with_cleanup_commits_calls_borg_with_cleanup_commits_parameter(): + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(COMPACT_COMMAND + ('--cleanup-commits', 'repo'), logging.INFO) module.compact_segments( - dry_run=False, repository='repo', storage_config={}, cleanup_commits=True, + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + cleanup_commits=True, ) def test_compact_segments_with_threshold_calls_borg_with_threshold_parameter(): + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(COMPACT_COMMAND + ('--threshold', '20', 'repo'), logging.INFO) module.compact_segments( - dry_run=False, repository='repo', storage_config={}, threshold=20, + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + threshold=20, ) def test_compact_segments_with_umask_calls_borg_with_umask_parameters(): storage_config = {'umask': '077'} + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(COMPACT_COMMAND + ('--umask', '077', 'repo'), logging.INFO) module.compact_segments( - dry_run=False, repository='repo', storage_config=storage_config, + dry_run=False, repository='repo', storage_config=storage_config, local_borg_version='1.2.3' ) def test_compact_segments_with_lock_wait_calls_borg_with_lock_wait_parameters(): storage_config = {'lock_wait': 5} + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(COMPACT_COMMAND + ('--lock-wait', '5', 'repo'), logging.INFO) module.compact_segments( - dry_run=False, repository='repo', storage_config=storage_config, + dry_run=False, repository='repo', storage_config=storage_config, local_borg_version='1.2.3' ) def test_compact_segments_with_extra_borg_options_calls_borg_with_extra_options(): + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(COMPACT_COMMAND + ('--extra', '--options', 'repo'), logging.INFO) module.compact_segments( dry_run=False, repository='repo', storage_config={'extra_borg_options': {'compact': '--extra --options'}}, + local_borg_version='1.2.3', ) diff --git a/tests/unit/borg/test_flags.py b/tests/unit/borg/test_flags.py index 3d1153ec3..db5425efe 100644 --- a/tests/unit/borg/test_flags.py +++ b/tests/unit/borg/test_flags.py @@ -45,3 +45,18 @@ def test_make_flags_from_arguments_omits_excludes(): arguments = flexmock(foo='bar', baz='quux') assert module.make_flags_from_arguments(arguments, excludes=('baz', 'other')) == ('foo', 'bar') + + +def test_make_repository_flags_with_borg_features_includes_repo_flag(): + flexmock(module.feature).should_receive('available').and_return(True) + + assert module.make_repository_flags(repository='repo', local_borg_version='1.2.3') == ( + '--repo', + 'repo', + ) + + +def test_make_repository_flags_without_borg_features_includes_omits_flag(): + flexmock(module.feature).should_receive('available').and_return(False) + + assert module.make_repository_flags(repository='repo', local_borg_version='1.2.3') == ('repo',) diff --git a/tests/unit/borg/test_prune.py b/tests/unit/borg/test_prune.py index 1ca794d70..e57ce7fce 100644 --- a/tests/unit/borg/test_prune.py +++ b/tests/unit/borg/test_prune.py @@ -67,7 +67,7 @@ def test_prune_archives_calls_borg_with_parameters(): flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(PRUNE_COMMAND + ('repo',), logging.INFO) module.prune_archives( @@ -79,29 +79,12 @@ def test_prune_archives_calls_borg_with_parameters(): ) -def test_prune_archives_with_borg_features_calls_borg_with_repo_flag(): - retention_config = flexmock() - flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( - BASE_PRUNE_FLAGS - ) - flexmock(module.feature).should_receive('available').and_return(True) - insert_execute_command_mock(PRUNE_COMMAND + ('--repo', 'repo'), logging.INFO) - - module.prune_archives( - dry_run=False, - repository='repo', - storage_config={}, - retention_config=retention_config, - local_borg_version='1.2.3', - ) - - def test_prune_archives_with_log_info_calls_borg_with_info_parameter(): retention_config = flexmock() flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(PRUNE_COMMAND + ('--info', 'repo'), logging.INFO) insert_logging_mock(logging.INFO) @@ -119,7 +102,7 @@ def test_prune_archives_with_log_debug_calls_borg_with_debug_parameter(): flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(PRUNE_COMMAND + ('--debug', '--show-rc', 'repo'), logging.INFO) insert_logging_mock(logging.DEBUG) @@ -137,7 +120,7 @@ def test_prune_archives_with_dry_run_calls_borg_with_dry_run_parameter(): flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(PRUNE_COMMAND + ('--dry-run', 'repo'), logging.INFO) module.prune_archives( @@ -154,7 +137,7 @@ def test_prune_archives_with_local_path_calls_borg_via_local_path(): flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(('borg1',) + PRUNE_COMMAND[1:] + ('repo',), logging.INFO) module.prune_archives( @@ -172,7 +155,7 @@ def test_prune_archives_with_remote_path_calls_borg_with_remote_path_parameters( flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(PRUNE_COMMAND + ('--remote-path', 'borg1', 'repo'), logging.INFO) module.prune_archives( @@ -190,7 +173,7 @@ def test_prune_archives_with_stats_calls_borg_with_stats_parameter_and_warning_o flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(PRUNE_COMMAND + ('--stats', 'repo'), logging.WARNING) module.prune_archives( @@ -208,7 +191,7 @@ def test_prune_archives_with_stats_and_log_info_calls_borg_with_stats_parameter_ flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_logging_mock(logging.INFO) insert_execute_command_mock(PRUNE_COMMAND + ('--stats', '--info', 'repo'), logging.INFO) @@ -227,7 +210,7 @@ def test_prune_archives_with_files_calls_borg_with_list_parameter_and_warning_ou flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(PRUNE_COMMAND + ('--list', 'repo'), logging.WARNING) module.prune_archives( @@ -245,7 +228,7 @@ def test_prune_archives_with_files_and_log_info_calls_borg_with_list_parameter_a flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_logging_mock(logging.INFO) insert_execute_command_mock(PRUNE_COMMAND + ('--info', '--list', 'repo'), logging.INFO) @@ -265,7 +248,7 @@ def test_prune_archives_with_umask_calls_borg_with_umask_parameters(): flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(PRUNE_COMMAND + ('--umask', '077', 'repo'), logging.INFO) module.prune_archives( @@ -283,7 +266,7 @@ def test_prune_archives_with_lock_wait_calls_borg_with_lock_wait_parameters(): flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(PRUNE_COMMAND + ('--lock-wait', '5', 'repo'), logging.INFO) module.prune_archives( @@ -300,7 +283,7 @@ def test_prune_archives_with_extra_borg_options_calls_borg_with_extra_options(): flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(PRUNE_COMMAND + ('--extra', '--options', 'repo'), logging.INFO) module.prune_archives( diff --git a/tests/unit/borg/test_rcreate.py b/tests/unit/borg/test_rcreate.py index 652de7b21..292534588 100644 --- a/tests/unit/borg/test_rcreate.py +++ b/tests/unit/borg/test_rcreate.py @@ -36,16 +36,7 @@ def test_create_repository_calls_borg_with_parameters(): insert_rinfo_command_not_found_mock() insert_rcreate_command_mock(RCREATE_COMMAND + ('--repo', 'repo')) flexmock(module.feature).should_receive('available').and_return(True) - - module.create_repository( - repository='repo', storage_config={}, local_borg_version='2.3.4', encryption_mode='repokey' - ) - - -def test_create_repository_without_borg_features_calls_borg_with_init_sub_command(): - insert_rinfo_command_not_found_mock() - insert_rcreate_command_mock(('borg', 'init', '--encryption', 'repokey', 'repo')) - flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) module.create_repository( repository='repo', storage_config={}, local_borg_version='2.3.4', encryption_mode='repokey' @@ -55,6 +46,7 @@ def test_create_repository_without_borg_features_calls_borg_with_init_sub_comman def test_create_repository_raises_for_borg_rcreate_error(): insert_rinfo_command_not_found_mock() flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').and_raise( module.subprocess.CalledProcessError(2, 'borg rcreate') @@ -72,6 +64,7 @@ def test_create_repository_raises_for_borg_rcreate_error(): def test_create_repository_skips_creation_when_repository_already_exists(): insert_rinfo_command_found_mock() flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) module.create_repository( repository='repo', storage_config={}, local_borg_version='2.3.4', encryption_mode='repokey' @@ -96,6 +89,7 @@ def test_create_repository_with_append_only_calls_borg_with_append_only_paramete insert_rinfo_command_not_found_mock() insert_rcreate_command_mock(RCREATE_COMMAND + ('--append-only', '--repo', 'repo')) flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) module.create_repository( repository='repo', @@ -110,6 +104,7 @@ def test_create_repository_with_storage_quota_calls_borg_with_storage_quota_para insert_rinfo_command_not_found_mock() insert_rcreate_command_mock(RCREATE_COMMAND + ('--storage-quota', '5G', '--repo', 'repo')) flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) module.create_repository( repository='repo', @@ -125,6 +120,7 @@ def test_create_repository_with_log_info_calls_borg_with_info_parameter(): insert_rcreate_command_mock(RCREATE_COMMAND + ('--info', '--repo', 'repo')) insert_logging_mock(logging.INFO) flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) module.create_repository( repository='repo', storage_config={}, local_borg_version='2.3.4', encryption_mode='repokey' @@ -136,6 +132,7 @@ def test_create_repository_with_log_debug_calls_borg_with_debug_parameter(): insert_rcreate_command_mock(RCREATE_COMMAND + ('--debug', '--repo', 'repo')) insert_logging_mock(logging.DEBUG) flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) module.create_repository( repository='repo', storage_config={}, local_borg_version='2.3.4', encryption_mode='repokey' @@ -146,6 +143,7 @@ def test_create_repository_with_local_path_calls_borg_via_local_path(): insert_rinfo_command_not_found_mock() insert_rcreate_command_mock(('borg1',) + RCREATE_COMMAND[1:] + ('--repo', 'repo')) flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) module.create_repository( repository='repo', @@ -160,6 +158,7 @@ def test_create_repository_with_remote_path_calls_borg_with_remote_path_paramete insert_rinfo_command_not_found_mock() insert_rcreate_command_mock(RCREATE_COMMAND + ('--remote-path', 'borg1', '--repo', 'repo')) flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) module.create_repository( repository='repo', @@ -174,6 +173,7 @@ def test_create_repository_with_extra_borg_options_calls_borg_with_extra_options insert_rinfo_command_not_found_mock() insert_rcreate_command_mock(RCREATE_COMMAND + ('--extra', '--options', '--repo', 'repo')) flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) module.create_repository( repository='repo', diff --git a/tests/unit/borg/test_rinfo.py b/tests/unit/borg/test_rinfo.py index 93f91c0ea..b3147b93d 100644 --- a/tests/unit/borg/test_rinfo.py +++ b/tests/unit/borg/test_rinfo.py @@ -9,6 +9,7 @@ from ..test_verbosity import insert_logging_mock def test_display_repository_info_calls_borg_with_parameters(): flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'rinfo', '--repo', 'repo'), @@ -27,6 +28,7 @@ def test_display_repository_info_calls_borg_with_parameters(): def test_display_repository_info_without_borg_features_calls_borg_with_info_sub_command(): flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'info', 'repo'), @@ -45,6 +47,7 @@ def test_display_repository_info_without_borg_features_calls_borg_with_info_sub_ def test_display_repository_info_with_log_info_calls_borg_with_info_parameter(): flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'rinfo', '--info', '--repo', 'repo'), @@ -63,6 +66,7 @@ def test_display_repository_info_with_log_info_calls_borg_with_info_parameter(): def test_display_repository_info_with_log_info_and_json_suppresses_most_borg_output(): flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'rinfo', '--json', '--repo', 'repo'), @@ -84,6 +88,7 @@ def test_display_repository_info_with_log_info_and_json_suppresses_most_borg_out def test_display_repository_info_with_log_debug_calls_borg_with_debug_parameter(): flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'rinfo', '--debug', '--show-rc', '--repo', 'repo'), @@ -103,6 +108,7 @@ def test_display_repository_info_with_log_debug_calls_borg_with_debug_parameter( def test_display_repository_info_with_log_debug_and_json_suppresses_most_borg_output(): flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'rinfo', '--json', '--repo', 'repo'), @@ -124,6 +130,7 @@ def test_display_repository_info_with_log_debug_and_json_suppresses_most_borg_ou def test_display_repository_info_with_json_calls_borg_with_json_parameter(): flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'rinfo', '--json', '--repo', 'repo'), @@ -144,6 +151,7 @@ def test_display_repository_info_with_json_calls_borg_with_json_parameter(): def test_display_repository_info_with_local_path_calls_borg_via_local_path(): flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg1', 'rinfo', '--repo', 'repo'), @@ -163,6 +171,7 @@ def test_display_repository_info_with_local_path_calls_borg_via_local_path(): def test_display_repository_info_with_remote_path_calls_borg_with_remote_path_parameters(): flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'rinfo', '--remote-path', 'borg1', '--repo', 'repo'), @@ -183,6 +192,7 @@ def test_display_repository_info_with_remote_path_calls_borg_with_remote_path_pa def test_display_repository_info_with_lock_wait_calls_borg_with_lock_wait_parameters(): storage_config = {'lock_wait': 5} flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'rinfo', '--lock-wait', '5', '--repo', 'repo'), From a05d0f378e47b9f1b032bf3990814a0301b8f0db Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Sat, 13 Aug 2022 22:50:14 -0700 Subject: [PATCH 15/38] Factor out repository/archive flags formatting code from create action (#557). --- borgmatic/borg/create.py | 8 +- borgmatic/borg/flags.py | 13 ++ tests/unit/borg/test_create.py | 302 +++++++++++++++------------------ tests/unit/borg/test_flags.py | 16 ++ 4 files changed, 164 insertions(+), 175 deletions(-) diff --git a/borgmatic/borg/create.py b/borgmatic/borg/create.py index c5a81c535..6cee09cba 100644 --- a/borgmatic/borg/create.py +++ b/borgmatic/borg/create.py @@ -5,7 +5,7 @@ import os import pathlib import tempfile -from borgmatic.borg import environment, feature, state +from borgmatic.borg import environment, feature, flags, state from borgmatic.execute import DO_NOT_CAPTURE, execute_command, execute_command_with_processes logger = logging.getLogger(__name__) @@ -298,11 +298,7 @@ def create_archive( + (('--progress',) if progress else ()) + (('--json',) if json else ()) + (tuple(extra_borg_options.split(' ')) if extra_borg_options else ()) - + ( - ('--repo', repository, archive_name_format) - if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version) - else (f'{repository}::{archive_name_format}',) - ) + + flags.make_repository_archive_flags(repository, archive_name_format, local_borg_version) + sources ) diff --git a/borgmatic/borg/flags.py b/borgmatic/borg/flags.py index d9e26d117..81b6a6b13 100644 --- a/borgmatic/borg/flags.py +++ b/borgmatic/borg/flags.py @@ -43,3 +43,16 @@ def make_repository_flags(repository, local_borg_version): if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version) else () ) + (repository,) + + +def make_repository_archive_flags(repository, archive, local_borg_version): + ''' + Given the path of a Borg repository, an archive name or pattern, and the local Borg version, + return Borg-version-appropriate command-line flags (as a tuple) for selecting that repository + and archive. + ''' + return ( + ('--repo', repository, archive) + if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version) + else (f'{repository}::{archive}',) + ) diff --git a/tests/unit/borg/test_create.py b/tests/unit/borg/test_create.py index 5b055461f..163000679 100644 --- a/tests/unit/borg/test_create.py +++ b/tests/unit/borg/test_create.py @@ -292,9 +292,9 @@ def test_create_archive_calls_borg_with_parameters(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS, @@ -318,44 +318,6 @@ def test_create_archive_calls_borg_with_parameters(): ) -def test_create_archive_with_borg_features_calls_borg_with_repo_flag(): - flexmock(module).should_receive('borgmatic_source_directories').and_return([]) - flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) - flexmock(module).should_receive('map_directories_to_devices').and_return({}) - flexmock(module).should_receive('expand_directories').and_return(()) - flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError) - flexmock(module).should_receive('expand_home_directories').and_return(()) - flexmock(module).should_receive('write_pattern_file').and_return(None) - flexmock(module.feature).should_receive('available').and_return(True) - flexmock(module).should_receive('ensure_files_readable') - flexmock(module).should_receive('make_pattern_flags').and_return(()) - flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(True) - flexmock(module.environment).should_receive('make_environment') - flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '--repo', 'repo', DEFAULT_ARCHIVE_NAME, 'foo', 'bar'), - output_log_level=logging.INFO, - output_file=None, - borg_local_path='borg', - working_directory=None, - extra_environment=None, - ) - - module.create_archive( - dry_run=False, - repository='repo', - location_config={ - 'source_directories': ['foo', 'bar'], - 'repositories': ['repo'], - 'exclude_patterns': None, - }, - storage_config={}, - local_borg_version='1.2.3', - ) - - def test_create_archive_calls_borg_with_environment(): flexmock(module).should_receive('borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) @@ -368,9 +330,9 @@ def test_create_archive_calls_borg_with_environment(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) environment = {'BORG_THINGY': 'YUP'} flexmock(module.environment).should_receive('make_environment').and_return(environment) flexmock(module).should_receive('execute_command').with_args( @@ -410,9 +372,9 @@ def test_create_archive_with_patterns_calls_borg_with_patterns(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(pattern_flags) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create') + pattern_flags + REPO_ARCHIVE_WITH_PATHS, @@ -451,9 +413,9 @@ def test_create_archive_with_exclude_patterns_calls_borg_with_excludes(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(exclude_flags) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create') + exclude_flags + REPO_ARCHIVE_WITH_PATHS, @@ -489,9 +451,9 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '--info') + REPO_ARCHIVE_WITH_PATHS, @@ -528,9 +490,9 @@ def test_create_archive_with_log_info_and_json_suppresses_most_borg_output(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS, @@ -568,9 +530,9 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '--debug', '--show-rc') + REPO_ARCHIVE_WITH_PATHS, @@ -607,9 +569,9 @@ def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS, @@ -647,9 +609,9 @@ def test_create_archive_with_dry_run_calls_borg_with_dry_run_parameter(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '--dry-run') + REPO_ARCHIVE_WITH_PATHS, @@ -687,9 +649,9 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats_paramete flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '--info', '--dry-run') + REPO_ARCHIVE_WITH_PATHS, @@ -727,9 +689,9 @@ def test_create_archive_with_checkpoint_interval_calls_borg_with_checkpoint_inte flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '--checkpoint-interval', '600') + REPO_ARCHIVE_WITH_PATHS, @@ -765,9 +727,9 @@ def test_create_archive_with_chunker_params_calls_borg_with_chunker_params_param flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '--chunker-params', '1,2,3,4') + REPO_ARCHIVE_WITH_PATHS, @@ -803,9 +765,9 @@ def test_create_archive_with_compression_calls_borg_with_compression_parameters( flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '--compression', 'rle') + REPO_ARCHIVE_WITH_PATHS, @@ -846,9 +808,9 @@ def test_create_archive_with_remote_rate_limit_calls_borg_with_upload_ratelimit_ flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', option_flag, '100') + REPO_ARCHIVE_WITH_PATHS, @@ -886,9 +848,9 @@ def test_create_archive_with_working_directory_calls_borg_with_working_directory flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS, @@ -925,9 +887,9 @@ def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_par flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '--one-file-system') + REPO_ARCHIVE_WITH_PATHS, @@ -969,9 +931,9 @@ def test_create_archive_with_numeric_owner_calls_borg_with_numeric_ids_parameter flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', option_flag) + REPO_ARCHIVE_WITH_PATHS, @@ -1008,9 +970,9 @@ def test_create_archive_with_read_special_calls_borg_with_read_special_parameter flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '--read-special') + REPO_ARCHIVE_WITH_PATHS, @@ -1054,9 +1016,9 @@ def test_create_archive_with_basic_option_calls_borg_with_corresponding_paramete flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create') + ((option_flag,) if option_flag else ()) + REPO_ARCHIVE_WITH_PATHS, @@ -1104,9 +1066,9 @@ def test_create_archive_with_atime_option_calls_borg_with_corresponding_paramete flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create') + ((option_flag,) if option_flag else ()) + REPO_ARCHIVE_WITH_PATHS, @@ -1154,9 +1116,9 @@ def test_create_archive_with_bsd_flags_option_calls_borg_with_corresponding_para flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create') + ((option_flag,) if option_flag else ()) + REPO_ARCHIVE_WITH_PATHS, @@ -1193,9 +1155,9 @@ def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters( flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '--files-cache', 'ctime,size') + REPO_ARCHIVE_WITH_PATHS, @@ -1232,9 +1194,9 @@ def test_create_archive_with_local_path_calls_borg_via_local_path(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg1', 'create') + REPO_ARCHIVE_WITH_PATHS, @@ -1271,9 +1233,9 @@ def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters( flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '--remote-path', 'borg1') + REPO_ARCHIVE_WITH_PATHS, @@ -1310,9 +1272,9 @@ def test_create_archive_with_umask_calls_borg_with_umask_parameters(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '--umask', '740') + REPO_ARCHIVE_WITH_PATHS, @@ -1348,9 +1310,9 @@ def test_create_archive_with_lock_wait_calls_borg_with_lock_wait_parameters(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '--lock-wait', '5') + REPO_ARCHIVE_WITH_PATHS, @@ -1386,9 +1348,9 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_warning_o flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '--stats') + REPO_ARCHIVE_WITH_PATHS, @@ -1425,9 +1387,9 @@ def test_create_archive_with_stats_and_log_info_calls_borg_with_stats_parameter_ flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '--info', '--stats') + REPO_ARCHIVE_WITH_PATHS, @@ -1465,9 +1427,9 @@ def test_create_archive_with_files_calls_borg_with_list_parameter_and_warning_ou flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '--list', '--filter', 'AME-') + REPO_ARCHIVE_WITH_PATHS, @@ -1504,9 +1466,9 @@ def test_create_archive_with_files_and_log_info_calls_borg_with_list_parameter_a flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '--list', '--filter', 'AME-', '--info') + REPO_ARCHIVE_WITH_PATHS, @@ -1544,9 +1506,9 @@ def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_para flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '--info', '--progress') + REPO_ARCHIVE_WITH_PATHS, @@ -1584,9 +1546,9 @@ def test_create_archive_with_progress_calls_borg_with_progress_parameter(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '--progress') + REPO_ARCHIVE_WITH_PATHS, @@ -1624,9 +1586,9 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command_with_processes').with_args( ('borg', 'create', '--one-file-system', '--read-special', '--progress') @@ -1666,9 +1628,9 @@ def test_create_archive_with_json_calls_borg_with_json_parameter(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS, @@ -1707,9 +1669,9 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_parameter() flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS, @@ -1749,9 +1711,9 @@ def test_create_archive_with_source_directories_glob_expands(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'food'), @@ -1788,9 +1750,9 @@ def test_create_archive_with_non_matching_source_directories_glob_passes_through flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo*'), @@ -1827,9 +1789,9 @@ def test_create_archive_with_glob_calls_borg_with_expanded_directories(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'food'), @@ -1865,9 +1827,9 @@ def test_create_archive_with_archive_name_format_calls_borg_with_archive_name(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::ARCHIVE_NAME',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', 'repo::ARCHIVE_NAME', 'foo', 'bar'), @@ -1892,6 +1854,7 @@ def test_create_archive_with_archive_name_format_calls_borg_with_archive_name(): def test_create_archive_with_archive_name_format_accepts_borg_placeholders(): + repository_archive_pattern = 'repo::Documents_{hostname}-{now}' flexmock(module).should_receive('borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) @@ -1903,12 +1866,12 @@ def test_create_archive_with_archive_name_format_accepts_borg_placeholders(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (repository_archive_pattern,) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', 'repo::Documents_{hostname}-{now}', 'foo', 'bar'), + ('borg', 'create', repository_archive_pattern, 'foo', 'bar'), output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -1930,6 +1893,7 @@ def test_create_archive_with_archive_name_format_accepts_borg_placeholders(): def test_create_archive_with_repository_accepts_borg_placeholders(): + repository_archive_pattern = '{fqdn}::Documents_{hostname}-{now}' flexmock(module).should_receive('borgmatic_source_directories').and_return([]) flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar')) flexmock(module).should_receive('map_directories_to_devices').and_return({}) @@ -1941,12 +1905,12 @@ def test_create_archive_with_repository_accepts_borg_placeholders(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (repository_archive_pattern,) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'create', '{fqdn}::Documents_{hostname}-{now}', 'foo', 'bar'), + ('borg', 'create', repository_archive_pattern, 'foo', 'bar'), output_log_level=logging.INFO, output_file=None, borg_local_path='borg', @@ -1979,9 +1943,9 @@ def test_create_archive_with_extra_borg_options_calls_borg_with_extra_options(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'create', '--extra', '--options') + REPO_ARCHIVE_WITH_PATHS, @@ -2018,9 +1982,9 @@ def test_create_archive_with_stream_processes_calls_borg_with_processes(): flexmock(module).should_receive('ensure_files_readable') flexmock(module).should_receive('make_pattern_flags').and_return(()) flexmock(module).should_receive('make_exclude_flags').and_return(()) - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, '1.2.3' - ).and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{DEFAULT_ARCHIVE_NAME}',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command_with_processes').with_args( ('borg', 'create', '--one-file-system', '--read-special') + REPO_ARCHIVE_WITH_PATHS, diff --git a/tests/unit/borg/test_flags.py b/tests/unit/borg/test_flags.py index db5425efe..7bf621dac 100644 --- a/tests/unit/borg/test_flags.py +++ b/tests/unit/borg/test_flags.py @@ -60,3 +60,19 @@ def test_make_repository_flags_without_borg_features_includes_omits_flag(): flexmock(module.feature).should_receive('available').and_return(False) assert module.make_repository_flags(repository='repo', local_borg_version='1.2.3') == ('repo',) + + +def test_make_repository_archive_flags_with_borg_features_separates_repository_and_archive(): + flexmock(module.feature).should_receive('available').and_return(True) + + assert module.make_repository_archive_flags( + repository='repo', archive='archive', local_borg_version='1.2.3' + ) == ('--repo', 'repo', 'archive',) + + +def test_make_repository_archive_flags_with_borg_features_joins_repository_and_archive(): + flexmock(module.feature).should_receive('available').and_return(False) + + assert module.make_repository_archive_flags( + repository='repo', archive='archive', local_borg_version='1.2.3' + ) == ('repo::archive',) From cce6d566612220f3e552e1445c6515b0c6ae930f Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Sat, 13 Aug 2022 23:07:29 -0700 Subject: [PATCH 16/38] Update extract action for Borg 2 support (#557). --- borgmatic/borg/extract.py | 8 +++-- tests/unit/borg/test_extract.py | 58 +++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/borgmatic/borg/extract.py b/borgmatic/borg/extract.py index 3f4df1350..c86f556a5 100644 --- a/borgmatic/borg/extract.py +++ b/borgmatic/borg/extract.py @@ -2,7 +2,7 @@ import logging import os import subprocess -from borgmatic.borg import environment, feature +from borgmatic.borg import environment, feature, flags from borgmatic.execute import DO_NOT_CAPTURE, execute_command logger = logging.getLogger(__name__) @@ -111,7 +111,11 @@ def extract_archive( + (('--strip-components', str(strip_components)) if strip_components else ()) + (('--progress',) if progress else ()) + (('--stdout',) if extract_to_stdout else ()) - + ('::'.join((repository if ':' in repository else os.path.abspath(repository), archive)),) + + flags.make_repository_archive_flags( + repository if ':' in repository else os.path.abspath(repository), + archive, + local_borg_version, + ) + (tuple(paths) if paths else ()) ) diff --git a/tests/unit/borg/test_extract.py b/tests/unit/borg/test_extract.py index 58503f7cc..9c8cfa449 100644 --- a/tests/unit/borg/test_extract.py +++ b/tests/unit/borg/test_extract.py @@ -28,6 +28,9 @@ def test_extract_last_archive_dry_run_calls_borg_with_last_archive(): ) insert_execute_command_mock(('borg', 'extract', '--dry-run', 'repo::archive2')) flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive2',) + ) module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=None) @@ -35,6 +38,7 @@ def test_extract_last_archive_dry_run_calls_borg_with_last_archive(): def test_extract_last_archive_dry_run_without_any_archives_should_not_raise(): insert_execute_command_output_mock(('borg', 'list', '--short', 'repo'), result='\n') flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(('repo',)) module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=None) @@ -46,6 +50,9 @@ def test_extract_last_archive_dry_run_with_log_info_calls_borg_with_info_paramet insert_execute_command_mock(('borg', 'extract', '--dry-run', '--info', 'repo::archive2')) insert_logging_mock(logging.INFO) flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive2',) + ) module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=None) @@ -59,6 +66,9 @@ def test_extract_last_archive_dry_run_with_log_debug_calls_borg_with_debug_param ) insert_logging_mock(logging.DEBUG) flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive2',) + ) module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=None) @@ -69,6 +79,9 @@ def test_extract_last_archive_dry_run_calls_borg_via_local_path(): ) insert_execute_command_mock(('borg1', 'extract', '--dry-run', 'repo::archive2')) flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive2',) + ) module.extract_last_archive_dry_run( storage_config={}, repository='repo', lock_wait=None, local_path='borg1' @@ -83,6 +96,9 @@ def test_extract_last_archive_dry_run_calls_borg_with_remote_path_parameters(): ('borg', 'extract', '--dry-run', '--remote-path', 'borg1', 'repo::archive2') ) flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive2',) + ) module.extract_last_archive_dry_run( storage_config={}, repository='repo', lock_wait=None, remote_path='borg1' @@ -97,6 +113,9 @@ def test_extract_last_archive_dry_run_calls_borg_with_lock_wait_parameters(): ('borg', 'extract', '--dry-run', '--lock-wait', '5', 'repo::archive2') ) flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive2',) + ) module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=5) @@ -105,6 +124,9 @@ def test_extract_archive_calls_borg_with_path_parameters(): flexmock(module.os.path).should_receive('abspath').and_return('repo') insert_execute_command_mock(('borg', 'extract', 'repo::archive', 'path1', 'path2')) flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) module.extract_archive( dry_run=False, @@ -121,6 +143,9 @@ def test_extract_archive_calls_borg_with_remote_path_parameters(): flexmock(module.os.path).should_receive('abspath').and_return('repo') insert_execute_command_mock(('borg', 'extract', '--remote-path', 'borg1', 'repo::archive')) flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) module.extract_archive( dry_run=False, @@ -141,6 +166,9 @@ def test_extract_archive_calls_borg_with_numeric_ids_parameter(feature_available flexmock(module.os.path).should_receive('abspath').and_return('repo') insert_execute_command_mock(('borg', 'extract', option_flag, 'repo::archive')) flexmock(module.feature).should_receive('available').and_return(feature_available) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) module.extract_archive( dry_run=False, @@ -157,6 +185,9 @@ def test_extract_archive_calls_borg_with_umask_parameters(): flexmock(module.os.path).should_receive('abspath').and_return('repo') insert_execute_command_mock(('borg', 'extract', '--umask', '0770', 'repo::archive')) flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) module.extract_archive( dry_run=False, @@ -173,6 +204,9 @@ def test_extract_archive_calls_borg_with_lock_wait_parameters(): flexmock(module.os.path).should_receive('abspath').and_return('repo') insert_execute_command_mock(('borg', 'extract', '--lock-wait', '5', 'repo::archive')) flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) module.extract_archive( dry_run=False, @@ -190,6 +224,9 @@ def test_extract_archive_with_log_info_calls_borg_with_info_parameter(): insert_execute_command_mock(('borg', 'extract', '--info', 'repo::archive')) insert_logging_mock(logging.INFO) flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) module.extract_archive( dry_run=False, @@ -209,6 +246,9 @@ def test_extract_archive_with_log_debug_calls_borg_with_debug_parameters(): ) insert_logging_mock(logging.DEBUG) flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) module.extract_archive( dry_run=False, @@ -225,6 +265,9 @@ def test_extract_archive_calls_borg_with_dry_run_parameter(): flexmock(module.os.path).should_receive('abspath').and_return('repo') insert_execute_command_mock(('borg', 'extract', '--dry-run', 'repo::archive')) flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) module.extract_archive( dry_run=True, @@ -241,6 +284,9 @@ def test_extract_archive_calls_borg_with_destination_path(): flexmock(module.os.path).should_receive('abspath').and_return('repo') insert_execute_command_mock(('borg', 'extract', 'repo::archive'), working_directory='/dest') flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) module.extract_archive( dry_run=False, @@ -258,6 +304,9 @@ def test_extract_archive_calls_borg_with_strip_components(): flexmock(module.os.path).should_receive('abspath').and_return('repo') insert_execute_command_mock(('borg', 'extract', '--strip-components', '5', 'repo::archive')) flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) module.extract_archive( dry_run=False, @@ -281,6 +330,9 @@ def test_extract_archive_calls_borg_with_progress_parameter(): extra_environment=None, ).once() flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) module.extract_archive( dry_run=False, @@ -323,6 +375,9 @@ def test_extract_archive_calls_borg_with_stdout_parameter_and_returns_process(): extra_environment=None, ).and_return(process).once() flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) assert ( module.extract_archive( @@ -346,6 +401,9 @@ def test_extract_archive_skips_abspath_for_remote_repository(): ('borg', 'extract', 'server:repo::archive'), working_directory=None, extra_environment=None, ).once() flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('server:repo::archive',) + ) module.extract_archive( dry_run=False, From cc04bf57dfedb26bfa663d512bfd97dc92962a91 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Mon, 15 Aug 2022 15:04:40 -0700 Subject: [PATCH 17/38] Update list action for Borg 2 support, add rinfo action, and update extract consistency check for Borg 2. --- borgmatic/borg/check.py | 2 +- borgmatic/borg/extract.py | 40 +-- borgmatic/borg/list.py | 150 ++++---- borgmatic/borg/rlist.py | 121 +++++++ borgmatic/commands/arguments.py | 49 ++- borgmatic/commands/borgmatic.py | 90 ++++- docs/Dockerfile | 2 +- docs/how-to/backup-your-databases.md | 7 +- docs/how-to/extract-a-backup.md | 7 +- docs/how-to/inspect-your-backups.md | 12 +- docs/how-to/monitor-your-backups.md | 6 +- docs/how-to/run-arbitrary-borg-commands.md | 9 +- tests/unit/borg/test_extract.py | 143 ++++---- tests/unit/borg/test_list.py | 391 +++++++++++++-------- tests/unit/borg/test_rlist.py | 381 ++++++++++++++++++++ tests/unit/commands/test_borgmatic.py | 33 +- 16 files changed, 1082 insertions(+), 361 deletions(-) create mode 100644 borgmatic/borg/rlist.py create mode 100644 tests/unit/borg/test_rlist.py diff --git a/borgmatic/borg/check.py b/borgmatic/borg/check.py index 917967413..e3dd0c32b 100644 --- a/borgmatic/borg/check.py +++ b/borgmatic/borg/check.py @@ -323,6 +323,6 @@ def check_archives( if 'extract' in checks: extract.extract_last_archive_dry_run( - storage_config, repository, lock_wait, local_path, remote_path + storage_config, local_borg_version, repository, lock_wait, local_path, remote_path ) write_check_time(make_check_time_path(location_config, borg_repository_id, 'extract')) diff --git a/borgmatic/borg/extract.py b/borgmatic/borg/extract.py index c86f556a5..5e2a58d2c 100644 --- a/borgmatic/borg/extract.py +++ b/borgmatic/borg/extract.py @@ -2,14 +2,19 @@ import logging import os import subprocess -from borgmatic.borg import environment, feature, flags +from borgmatic.borg import environment, feature, flags, rlist from borgmatic.execute import DO_NOT_CAPTURE, execute_command logger = logging.getLogger(__name__) def extract_last_archive_dry_run( - storage_config, repository, lock_wait=None, local_path='borg', remote_path=None + storage_config, + local_borg_version, + repository, + lock_wait=None, + local_path='borg', + remote_path=None, ): ''' Perform an extraction dry-run of the most recent archive. If there are no archives, skip the @@ -23,40 +28,23 @@ def extract_last_archive_dry_run( elif logger.isEnabledFor(logging.INFO): verbosity_flags = ('--info',) - full_list_command = ( - (local_path, 'list', '--short') - + remote_path_flags - + lock_wait_flags - + verbosity_flags - + (repository,) - ) - - borg_environment = environment.make_environment(storage_config) - - list_output = execute_command( - full_list_command, - output_log_level=None, - borg_local_path=local_path, - extra_environment=borg_environment, - ) - try: - last_archive_name = list_output.strip().splitlines()[-1] - except IndexError: + last_archive_name = rlist.resolve_archive_name( + repository, 'latest', storage_config, local_borg_version, local_path, remote_path + ) + except ValueError: + logger.warning('No archives found. Skipping extract consistency check.') return list_flag = ('--list',) if logger.isEnabledFor(logging.DEBUG) else () + borg_environment = environment.make_environment(storage_config) full_extract_command = ( (local_path, 'extract', '--dry-run') + remote_path_flags + lock_wait_flags + verbosity_flags + list_flag - + ( - '{repository}::{last_archive_name}'.format( - repository=repository, last_archive_name=last_archive_name - ), - ) + + flags.make_repository_archive_flags(repository, last_archive_name, local_borg_version) ) execute_command( diff --git a/borgmatic/borg/list.py b/borgmatic/borg/list.py index 1d897ec73..fadc5055d 100644 --- a/borgmatic/borg/list.py +++ b/borgmatic/borg/list.py @@ -1,58 +1,24 @@ +import argparse import copy import logging import re -from borgmatic.borg import environment -from borgmatic.borg.flags import make_flags, make_flags_from_arguments +from borgmatic.borg import environment, feature, flags, rlist from borgmatic.execute import execute_command logger = logging.getLogger(__name__) -def resolve_archive_name(repository, archive, storage_config, local_path='borg', remote_path=None): - ''' - Given a local or remote repository path, an archive name, a storage config dict, a local Borg - path, and a remote Borg path, simply return the archive name. But if the archive name is - "latest", then instead introspect the repository for the latest archive and return its name. - - Raise ValueError if "latest" is given but there are no archives in the repository. - ''' - if archive != "latest": - return archive - - lock_wait = storage_config.get('lock_wait', None) - - full_command = ( - (local_path, 'list') - + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ()) - + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ()) - + make_flags('remote-path', remote_path) - + make_flags('lock-wait', lock_wait) - + make_flags('last', 1) - + ('--short', repository) - ) - - output = execute_command( - full_command, - output_log_level=None, - borg_local_path=local_path, - extra_environment=environment.make_environment(storage_config), - ) - try: - latest_archive = output.strip().splitlines()[-1] - except IndexError: - raise ValueError('No archives found in the repository') - - logger.debug('{}: Latest archive is {}'.format(repository, latest_archive)) - - return latest_archive - - MAKE_FLAGS_EXCLUDES = ('repository', 'archive', 'successful', 'paths', 'find_paths') def make_list_command( - repository, storage_config, list_arguments, local_path='borg', remote_path=None + repository, + storage_config, + local_borg_version, + list_arguments, + local_path='borg', + remote_path=None, ): ''' Given a local or remote repository path, a storage config dict, the arguments to the list @@ -73,13 +39,15 @@ def make_list_command( if logger.isEnabledFor(logging.DEBUG) and not list_arguments.json else () ) - + make_flags('remote-path', remote_path) - + make_flags('lock-wait', lock_wait) - + make_flags_from_arguments(list_arguments, excludes=MAKE_FLAGS_EXCLUDES,) + + flags.make_flags('remote-path', remote_path) + + flags.make_flags('lock-wait', lock_wait) + + flags.make_flags_from_arguments(list_arguments, excludes=MAKE_FLAGS_EXCLUDES,) + ( - ('::'.join((repository, list_arguments.archive)),) + flags.make_repository_archive_flags( + repository, list_arguments.archive, local_borg_version + ) if list_arguments.archive - else (repository,) + else flags.make_repository_flags(repository, local_borg_version) ) + (tuple(list_arguments.paths) if list_arguments.paths else ()) ) @@ -109,29 +77,76 @@ def make_find_paths(find_paths): ) -def list_archives(repository, storage_config, list_arguments, local_path='borg', remote_path=None): +def list_archive( + repository, + storage_config, + local_borg_version, + list_arguments, + local_path='borg', + remote_path=None, +): ''' - Given a local or remote repository path, a storage config dict, the arguments to the list - action, and local and remote Borg paths, display the output of listing Borg archives in the - repository or return JSON output. Or, if an archive name is given, list the files in that - archive. Or, if list_arguments.find_paths are given, list the files by searching across multiple - archives. + Given a local or remote repository path, a storage config dict, the local Borg version, the + arguments to the list action, and local and remote Borg paths, display the output of listing + the files of a Borg archive (or return JSON output). If list_arguments.find_paths are given, + list the files by searching across multiple archives. If neither find_paths nor archive name + are given, instead list the archives in the given repository. ''' + if not list_arguments.archive and not list_arguments.find_paths: + if feature.available(feature.Feature.RLIST, local_borg_version): + logger.warning( + 'Omitting the --archive flag on the list action is deprecated when using Borg 2.x. Use the rlist action instead.' + ) + + rlist_arguments = argparse.Namespace( + repository=repository, + short=list_arguments.short, + format=list_arguments.format, + json=list_arguments.json, + prefix=list_arguments.prefix, + glob_archives=list_arguments.glob_archives, + sort_by=list_arguments.sort_by, + first=list_arguments.first, + last=list_arguments.last, + ) + return rlist.list_repository( + repository, storage_config, local_borg_version, rlist_arguments, local_path, remote_path + ) + + if feature.available(feature.Feature.RLIST, local_borg_version): + for flag_name in ('prefix', 'glob-archives', 'sort-by', 'first', 'last'): + if getattr(list_arguments, flag_name.replace('-', '_'), None): + raise ValueError( + f'The --{flag_name} flag on the list action is not supported when using the --archive flag and Borg 2.x.' + ) + borg_environment = environment.make_environment(storage_config) # If there are any paths to find (and there's not a single archive already selected), start by # getting a list of archives to search. if list_arguments.find_paths and not list_arguments.archive: - repository_arguments = copy.copy(list_arguments) - repository_arguments.archive = None - repository_arguments.json = False - repository_arguments.format = None + rlist_arguments = argparse.Namespace( + repository=repository, + short=True, + format=None, + json=None, + prefix=list_arguments.prefix, + glob_archives=list_arguments.glob_archives, + sort_by=list_arguments.sort_by, + first=list_arguments.first, + last=list_arguments.last, + ) # Ask Borg to list archives. Capture its output for use below. archive_lines = tuple( execute_command( - make_list_command( - repository, storage_config, repository_arguments, local_path, remote_path + rlist.make_rlist_command( + repository, + storage_config, + local_borg_version, + rlist_arguments, + local_path, + remote_path, ), output_log_level=None, borg_local_path=local_path, @@ -144,19 +159,18 @@ def list_archives(repository, storage_config, list_arguments, local_path='borg', archive_lines = (list_arguments.archive,) # For each archive listed by Borg, run list on the contents of that archive. - for archive_line in archive_lines: - try: - archive = archive_line.split()[0] - except (AttributeError, IndexError): - archive = None - - if archive: - logger.warning(archive_line) + for archive in archive_lines: + logger.warning(f'{repository}: Listing archive {archive}') archive_arguments = copy.copy(list_arguments) archive_arguments.archive = archive main_command = make_list_command( - repository, storage_config, archive_arguments, local_path, remote_path + repository, + storage_config, + local_borg_version, + archive_arguments, + local_path, + remote_path, ) + make_find_paths(list_arguments.find_paths) output = execute_command( diff --git a/borgmatic/borg/rlist.py b/borgmatic/borg/rlist.py new file mode 100644 index 000000000..a4cd1ef3e --- /dev/null +++ b/borgmatic/borg/rlist.py @@ -0,0 +1,121 @@ +import logging + +from borgmatic.borg import environment, feature, flags +from borgmatic.execute import execute_command + +logger = logging.getLogger(__name__) + + +def resolve_archive_name( + repository, archive, storage_config, local_borg_version, local_path='borg', remote_path=None +): + ''' + Given a local or remote repository path, an archive name, a storage config dict, a local Borg + path, and a remote Borg path, simply return the archive name. But if the archive name is + "latest", then instead introspect the repository for the latest archive and return its name. + + Raise ValueError if "latest" is given but there are no archives in the repository. + ''' + if archive != "latest": + return archive + + lock_wait = storage_config.get('lock_wait', None) + + full_command = ( + ( + local_path, + 'rlist' if feature.available(feature.Feature.RLIST, local_borg_version) else 'list', + ) + + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ()) + + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ()) + + flags.make_flags('remote-path', remote_path) + + flags.make_flags('lock-wait', lock_wait) + + flags.make_flags('last', 1) + + ('--short',) + + flags.make_repository_flags(repository, local_borg_version) + ) + + output = execute_command( + full_command, + output_log_level=None, + borg_local_path=local_path, + extra_environment=environment.make_environment(storage_config), + ) + try: + latest_archive = output.strip().splitlines()[-1] + except IndexError: + raise ValueError('No archives found in the repository') + + logger.debug('{}: Latest archive is {}'.format(repository, latest_archive)) + + return latest_archive + + +MAKE_FLAGS_EXCLUDES = ('repository',) + + +def make_rlist_command( + repository, + storage_config, + local_borg_version, + rlist_arguments, + local_path='borg', + remote_path=None, +): + ''' + Given a local or remote repository path, a storage config dict, the local Borg version, the + arguments to the rlist action, and local and remote Borg paths, return a command as a tuple to + list archives with a repository. + ''' + lock_wait = storage_config.get('lock_wait', None) + + return ( + ( + local_path, + 'rlist' if feature.available(feature.Feature.RLIST, local_borg_version) else 'list', + ) + + ( + ('--info',) + if logger.getEffectiveLevel() == logging.INFO and not rlist_arguments.json + else () + ) + + ( + ('--debug', '--show-rc') + if logger.isEnabledFor(logging.DEBUG) and not rlist_arguments.json + else () + ) + + flags.make_flags('remote-path', remote_path) + + flags.make_flags('lock-wait', lock_wait) + + flags.make_flags_from_arguments(rlist_arguments, excludes=MAKE_FLAGS_EXCLUDES,) + + flags.make_repository_flags(repository, local_borg_version) + ) + + +def list_repository( + repository, + storage_config, + local_borg_version, + rlist_arguments, + local_path='borg', + remote_path=None, +): + ''' + Given a local or remote repository path, a storage config dict, the local Borg version, the + arguments to the list action, and local and remote Borg paths, display the output of listing + Borg archives in the given repository (or return JSON output). + ''' + borg_environment = environment.make_environment(storage_config) + + main_command = make_rlist_command( + repository, storage_config, local_borg_version, rlist_arguments, local_path, remote_path + ) + + output = execute_command( + main_command, + output_log_level=None if rlist_arguments.json else logging.WARNING, + borg_local_path=local_path, + extra_environment=borg_environment, + ) + + if rlist_arguments.json: + return output diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index f4144531e..02180def7 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -14,6 +14,7 @@ SUBPARSER_ALIASES = { 'mount': ['--mount', '-m'], 'umount': ['--umount', '-u'], 'restore': ['--restore', '-r'], + 'rlist': [], 'list': ['--list', '-l'], 'rinfo': [], 'info': ['--info', '-i'], @@ -546,18 +547,54 @@ def make_parsers(): '-h', '--help', action='help', help='Show this help message and exit' ) + rlist_parser = subparsers.add_parser( + 'rlist', + aliases=SUBPARSER_ALIASES['rlist'], + help='List repository', + description='List the archives in a repository', + add_help=False, + ) + rlist_group = rlist_parser.add_argument_group('rlist arguments') + rlist_group.add_argument( + '--repository', help='Path of repository to list, defaults to the configured repositories', + ) + rlist_group.add_argument( + '--short', default=False, action='store_true', help='Output only archive names' + ) + rlist_group.add_argument('--format', help='Format for archive listing') + rlist_group.add_argument( + '--json', default=False, action='store_true', help='Output results as JSON' + ) + rlist_group.add_argument( + '-P', '--prefix', help='Only list archive names starting with this prefix' + ) + rlist_group.add_argument( + '-a', '--glob-archives', metavar='GLOB', help='Only list archive names matching this glob' + ) + rlist_group.add_argument( + '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys' + ) + rlist_group.add_argument( + '--first', metavar='N', help='List first N archives after other filters are applied' + ) + rlist_group.add_argument( + '--last', metavar='N', help='List last N archives after other filters are applied' + ) + rlist_group.add_argument('-h', '--help', action='help', help='Show this help message and exit') + list_parser = subparsers.add_parser( 'list', aliases=SUBPARSER_ALIASES['list'], - help='List archives', - description='List archives or the contents of an archive', + help='List archive', + description='List the files in an archive or search for a file across archives', add_help=False, ) list_group = list_parser.add_argument_group('list arguments') list_group.add_argument( - '--repository', help='Path of repository to list, defaults to the configured repositories', + '--repository', + help='Path of repository containing archive to list, defaults to the configured repositories', ) - list_group.add_argument('--archive', help='Name of archive to list (or "latest")') + list_group.add_argument('--archive', help='Name of the archive to list (or "latest")') list_group.add_argument( '--path', metavar='PATH', @@ -573,7 +610,7 @@ def make_parsers(): help='Partial paths or patterns to search for and list across multiple archives', ) list_group.add_argument( - '--short', default=False, action='store_true', help='Output only archive or path names' + '--short', default=False, action='store_true', help='Output only path names' ) list_group.add_argument('--format', help='Format for file listing') list_group.add_argument( @@ -589,7 +626,7 @@ def make_parsers(): '--successful', default=True, action='store_true', - help='Deprecated in favor of listing successful (non-checkpoint) backups by default in newer versions of Borg', + help='Deprecated; no effect. Newer versions of Borg list successful (non-checkpoint) archives by default.', ) list_group.add_argument( '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys' diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index f806e4d35..e4455daa9 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -25,6 +25,7 @@ from borgmatic.borg import mount as borg_mount from borgmatic.borg import prune as borg_prune from borgmatic.borg import rcreate as borg_rcreate from borgmatic.borg import rinfo as borg_rinfo +from borgmatic.borg import rlist as borg_rlist from borgmatic.borg import umount as borg_umount from borgmatic.borg import version as borg_version from borgmatic.commands.arguments import parse_arguments @@ -434,8 +435,13 @@ def run_actions( borg_extract.extract_archive( global_arguments.dry_run, repository, - borg_list.resolve_archive_name( - repository, arguments['extract'].archive, storage, local_path, remote_path + borg_rlist.resolve_archive_name( + repository, + arguments['extract'].archive, + storage, + local_borg_version, + local_path, + remote_path, ), arguments['extract'].paths, location, @@ -467,8 +473,13 @@ def run_actions( borg_export_tar.export_tar_archive( global_arguments.dry_run, repository, - borg_list.resolve_archive_name( - repository, arguments['export-tar'].archive, storage, local_path, remote_path + borg_rlist.resolve_archive_name( + repository, + arguments['export-tar'].archive, + storage, + local_borg_version, + local_path, + remote_path, ), arguments['export-tar'].paths, arguments['export-tar'].destination, @@ -492,8 +503,13 @@ def run_actions( borg_mount.mount_archive( repository, - borg_list.resolve_archive_name( - repository, arguments['mount'].archive, storage, local_path, remote_path + borg_rlist.resolve_archive_name( + repository, + arguments['mount'].archive, + storage, + local_borg_version, + local_path, + remote_path, ), arguments['mount'].mount_point, arguments['mount'].paths, @@ -525,8 +541,13 @@ def run_actions( if 'all' in restore_names: restore_names = [] - archive_name = borg_list.resolve_archive_name( - repository, arguments['restore'].archive, storage, local_path, remote_path + archive_name = borg_rlist.resolve_archive_name( + repository, + arguments['restore'].archive, + storage, + local_borg_version, + local_path, + remote_path, ) found_names = set() @@ -596,20 +617,45 @@ def run_actions( ', '.join(missing_names) ) ) - + if 'rlist' in arguments: + if arguments['rlist'].repository is None or validate.repositories_match( + repository, arguments['rlist'].repository + ): + rlist_arguments = copy.copy(arguments['rlist']) + if not rlist_arguments.json: # pragma: nocover + logger.warning('{}: Listing repository'.format(repository)) + json_output = borg_rlist.list_repository( + repository, + storage, + local_borg_version, + rlist_arguments=rlist_arguments, + local_path=local_path, + remote_path=remote_path, + ) + if json_output: # pragma: nocover + yield json.loads(json_output) if 'list' in arguments: if arguments['list'].repository is None or validate.repositories_match( repository, arguments['list'].repository ): list_arguments = copy.copy(arguments['list']) if not list_arguments.json: # pragma: nocover - logger.warning('{}: Listing archives'.format(repository)) - list_arguments.archive = borg_list.resolve_archive_name( - repository, list_arguments.archive, storage, local_path, remote_path + if list_arguments.find_paths: + logger.warning('{}: Searching archives'.format(repository)) + else: + logger.warning('{}: Listing archive'.format(repository)) + list_arguments.archive = borg_rlist.resolve_archive_name( + repository, + list_arguments.archive, + storage, + local_borg_version, + local_path, + remote_path, ) - json_output = borg_list.list_archives( + json_output = borg_list.list_archive( repository, storage, + local_borg_version, list_arguments=list_arguments, local_path=local_path, remote_path=remote_path, @@ -640,8 +686,13 @@ def run_actions( info_arguments = copy.copy(arguments['info']) if not info_arguments.json: # pragma: nocover logger.warning('{}: Displaying archive summary information'.format(repository)) - info_arguments.archive = borg_list.resolve_archive_name( - repository, info_arguments.archive, storage, local_path, remote_path + info_arguments.archive = borg_rlist.resolve_archive_name( + repository, + info_arguments.archive, + storage, + local_borg_version, + local_path, + remote_path, ) json_output = borg_info.display_archives_info( repository, @@ -658,8 +709,13 @@ def run_actions( repository, arguments['borg'].repository ): logger.warning('{}: Running arbitrary Borg command'.format(repository)) - archive_name = borg_list.resolve_archive_name( - repository, arguments['borg'].archive, storage, local_path, remote_path + archive_name = borg_rlist.resolve_archive_name( + repository, + arguments['borg'].archive, + storage, + local_borg_version, + local_path, + remote_path, ) borg_borg.run_arbitrary_borg( repository, diff --git a/docs/Dockerfile b/docs/Dockerfile index 89cb1862d..35d50b77e 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -4,7 +4,7 @@ COPY . /app RUN apk add --no-cache py3-pip py3-ruamel.yaml py3-ruamel.yaml.clib RUN pip install --no-cache /app && generate-borgmatic-config && chmod +r /etc/borgmatic/config.yaml RUN borgmatic --help > /command-line.txt \ - && for action in init prune compact create check extract export-tar mount umount restore list info borg; do \ + && for action in rcreate prune compact create check extract export-tar mount umount restore rlist list rinfo info borg; do \ echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \ && borgmatic "$action" --help >> /command-line.txt; done diff --git a/docs/how-to/backup-your-databases.md b/docs/how-to/backup-your-databases.md index 01a84f963..6a485289a 100644 --- a/docs/how-to/backup-your-databases.md +++ b/docs/how-to/backup-your-databases.md @@ -133,14 +133,13 @@ that you'd like supported. To restore a database dump from an archive, use the `borgmatic restore` action. But the first step is to figure out which archive to restore from. A -good way to do that is to use the `list` action: +good way to do that is to use the `rlist` action: ```bash -borgmatic list +borgmatic rlist ``` -(No borgmatic `list` action? Try the old-style `--list`, or upgrade -borgmatic!) +(No borgmatic `rlist` action? Try `list` instead or upgrade borgmatic!) That should yield output looking something like: diff --git a/docs/how-to/extract-a-backup.md b/docs/how-to/extract-a-backup.md index 0dd47b0cc..a8ae9c544 100644 --- a/docs/how-to/extract-a-backup.md +++ b/docs/how-to/extract-a-backup.md @@ -9,14 +9,13 @@ eleventyNavigation: When the worst happens—or you want to test your backups—the first step is to figure out which archive to extract. A good way to do that is to use the -`list` action: +`rlist` action: ```bash -borgmatic list +borgmatic rlist ``` -(No borgmatic `list` action? Try the old-style `--list`, or upgrade -borgmatic!) +(No borgmatic `rlist` action? Try `list` instead or upgrade borgmatic!) That should yield output looking something like: diff --git a/docs/how-to/inspect-your-backups.md b/docs/how-to/inspect-your-backups.md index b266745f2..48ab194a9 100644 --- a/docs/how-to/inspect-your-backups.md +++ b/docs/how-to/inspect-your-backups.md @@ -46,14 +46,20 @@ borgmatic list borgmatic info ``` -New in borgmatic version 2.0.0 -There's also an `rinfo` action for displaying repository information with Borg -2.x: +New in borgmatic version 1.7.0 +There are also `rlist` and `rinfo` actions for displaying repository +information with Borg 2.x: ```bash +borgmatic rlist borgmatic rinfo ``` +See the [borgmatic command-line +reference](https://torsion.org/borgmatic/docs/reference/command-line/) for +more information. + + ### Searching for a file New in version 1.6.3 Let's say diff --git a/docs/how-to/monitor-your-backups.md b/docs/how-to/monitor-your-backups.md index 3dabb7de2..f8c21bb26 100644 --- a/docs/how-to/monitor-your-backups.md +++ b/docs/how-to/monitor-your-backups.md @@ -329,9 +329,9 @@ output only shows up at the console, and not in syslog. ### Latest backups -All borgmatic actions that accept an "--archive" flag allow you to specify an -archive name of "latest". This lets you get the latest archive without having -to first run "borgmatic list" manually, which can be handy in automated +All borgmatic actions that accept an `--archive` flag allow you to specify an +archive name of `latest`. This lets you get the latest archive without having +to first run `borgmatic rlist` manually, which can be handy in automated scripts. Here's an example: ```bash diff --git a/docs/how-to/run-arbitrary-borg-commands.md b/docs/how-to/run-arbitrary-borg-commands.md index f0e152947..8ffddebf7 100644 --- a/docs/how-to/run-arbitrary-borg-commands.md +++ b/docs/how-to/run-arbitrary-borg-commands.md @@ -46,12 +46,11 @@ options, as that part is provided by borgmatic. You can also specify Borg options for relevant commands: ```bash -borgmatic borg list --progress +borgmatic borg rlist --short ``` -This runs Borg's `list` command once on each configured borgmatic -repository. However, the native `borgmatic list` action should be preferred -for most use. +This runs Borg's `rlist` command once on each configured borgmatic repository. +However, the native `borgmatic rlist` action should be preferred for most use. What if you only want to run Borg on a single configured borgmatic repository when you've got several configured? Not a problem. @@ -63,7 +62,7 @@ borgmatic borg --repository repo.borg break-lock And what about a single archive? ```bash -borgmatic borg --archive your-archive-name list +borgmatic borg --archive your-archive-name rlist ``` ### Limitations diff --git a/tests/unit/borg/test_extract.py b/tests/unit/borg/test_extract.py index 9c8cfa449..8e54dbc03 100644 --- a/tests/unit/borg/test_extract.py +++ b/tests/unit/borg/test_extract.py @@ -23,101 +23,100 @@ def insert_execute_command_output_mock(command, result): def test_extract_last_archive_dry_run_calls_borg_with_last_archive(): - insert_execute_command_output_mock( - ('borg', 'list', '--short', 'repo'), result='archive1\narchive2\n' - ) - insert_execute_command_mock(('borg', 'extract', '--dry-run', 'repo::archive2')) - flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.rlist).should_receive('resolve_archive_name').and_return('archive') + insert_execute_command_mock(('borg', 'extract', '--dry-run', 'repo::archive')) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( - ('repo::archive2',) - ) - - module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=None) - - -def test_extract_last_archive_dry_run_without_any_archives_should_not_raise(): - insert_execute_command_output_mock(('borg', 'list', '--short', 'repo'), result='\n') - flexmock(module.feature).should_receive('available').and_return(True) - flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(('repo',)) - - module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=None) - - -def test_extract_last_archive_dry_run_with_log_info_calls_borg_with_info_parameter(): - insert_execute_command_output_mock( - ('borg', 'list', '--short', '--info', 'repo'), result='archive1\narchive2\n' - ) - insert_execute_command_mock(('borg', 'extract', '--dry-run', '--info', 'repo::archive2')) - insert_logging_mock(logging.INFO) - flexmock(module.feature).should_receive('available').and_return(True) - flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( - ('repo::archive2',) - ) - - module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=None) - - -def test_extract_last_archive_dry_run_with_log_debug_calls_borg_with_debug_parameter(): - insert_execute_command_output_mock( - ('borg', 'list', '--short', '--debug', '--show-rc', 'repo'), result='archive1\narchive2\n' - ) - insert_execute_command_mock( - ('borg', 'extract', '--dry-run', '--debug', '--show-rc', '--list', 'repo::archive2') - ) - insert_logging_mock(logging.DEBUG) - flexmock(module.feature).should_receive('available').and_return(True) - flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( - ('repo::archive2',) - ) - - module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=None) - - -def test_extract_last_archive_dry_run_calls_borg_via_local_path(): - insert_execute_command_output_mock( - ('borg1', 'list', '--short', 'repo'), result='archive1\narchive2\n' - ) - insert_execute_command_mock(('borg1', 'extract', '--dry-run', 'repo::archive2')) - flexmock(module.feature).should_receive('available').and_return(True) - flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( - ('repo::archive2',) + ('repo::archive',) ) module.extract_last_archive_dry_run( - storage_config={}, repository='repo', lock_wait=None, local_path='borg1' + storage_config={}, local_borg_version='1.2.3', repository='repo', lock_wait=None + ) + + +def test_extract_last_archive_dry_run_without_any_archives_should_not_raise(): + flexmock(module.rlist).should_receive('resolve_archive_name').and_raise(ValueError) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(('repo',)) + + module.extract_last_archive_dry_run( + storage_config={}, local_borg_version='1.2.3', repository='repo', lock_wait=None + ) + + +def test_extract_last_archive_dry_run_with_log_info_calls_borg_with_info_parameter(): + flexmock(module.rlist).should_receive('resolve_archive_name').and_return('archive') + insert_execute_command_mock(('borg', 'extract', '--dry-run', '--info', 'repo::archive')) + insert_logging_mock(logging.INFO) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) + + module.extract_last_archive_dry_run( + storage_config={}, local_borg_version='1.2.3', repository='repo', lock_wait=None + ) + + +def test_extract_last_archive_dry_run_with_log_debug_calls_borg_with_debug_parameter(): + flexmock(module.rlist).should_receive('resolve_archive_name').and_return('archive') + insert_execute_command_mock( + ('borg', 'extract', '--dry-run', '--debug', '--show-rc', '--list', 'repo::archive') + ) + insert_logging_mock(logging.DEBUG) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) + + module.extract_last_archive_dry_run( + storage_config={}, local_borg_version='1.2.3', repository='repo', lock_wait=None + ) + + +def test_extract_last_archive_dry_run_calls_borg_via_local_path(): + flexmock(module.rlist).should_receive('resolve_archive_name').and_return('archive') + insert_execute_command_mock(('borg1', 'extract', '--dry-run', 'repo::archive')) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) + + module.extract_last_archive_dry_run( + storage_config={}, + local_borg_version='1.2.3', + repository='repo', + lock_wait=None, + local_path='borg1', ) def test_extract_last_archive_dry_run_calls_borg_with_remote_path_parameters(): - insert_execute_command_output_mock( - ('borg', 'list', '--short', '--remote-path', 'borg1', 'repo'), result='archive1\narchive2\n' - ) + flexmock(module.rlist).should_receive('resolve_archive_name').and_return('archive') insert_execute_command_mock( - ('borg', 'extract', '--dry-run', '--remote-path', 'borg1', 'repo::archive2') + ('borg', 'extract', '--dry-run', '--remote-path', 'borg1', 'repo::archive') ) - flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( - ('repo::archive2',) + ('repo::archive',) ) module.extract_last_archive_dry_run( - storage_config={}, repository='repo', lock_wait=None, remote_path='borg1' + storage_config={}, + local_borg_version='1.2.3', + repository='repo', + lock_wait=None, + remote_path='borg1', ) def test_extract_last_archive_dry_run_calls_borg_with_lock_wait_parameters(): - insert_execute_command_output_mock( - ('borg', 'list', '--short', '--lock-wait', '5', 'repo'), result='archive1\narchive2\n' - ) + flexmock(module.rlist).should_receive('resolve_archive_name').and_return('archive') insert_execute_command_mock( - ('borg', 'extract', '--dry-run', '--lock-wait', '5', 'repo::archive2') + ('borg', 'extract', '--dry-run', '--lock-wait', '5', 'repo::archive') ) - flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( - ('repo::archive2',) + ('repo::archive',) ) - module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=5) + module.extract_last_archive_dry_run( + storage_config={}, local_borg_version='1.2.3', repository='repo', lock_wait=5 + ) def test_extract_archive_calls_borg_with_path_parameters(): diff --git a/tests/unit/borg/test_list.py b/tests/unit/borg/test_list.py index 6123a077b..7111cb61a 100644 --- a/tests/unit/borg/test_list.py +++ b/tests/unit/borg/test_list.py @@ -8,129 +8,17 @@ from borgmatic.borg import list as module from ..test_verbosity import insert_logging_mock -BORG_LIST_LATEST_ARGUMENTS = ( - '--last', - '1', - '--short', - 'repo', -) - - -def test_resolve_archive_name_passes_through_non_latest_archive_name(): - archive = 'myhost-2030-01-01T14:41:17.647620' - - assert module.resolve_archive_name('repo', archive, storage_config={}) == archive - - -def test_resolve_archive_name_calls_borg_with_parameters(): - expected_archive = 'archive-name' - flexmock(module.environment).should_receive('make_environment') - flexmock(module).should_receive('execute_command').with_args( - ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS, - output_log_level=None, - borg_local_path='borg', - extra_environment=None, - ).and_return(expected_archive + '\n') - - assert module.resolve_archive_name('repo', 'latest', storage_config={}) == expected_archive - - -def test_resolve_archive_name_with_log_info_calls_borg_with_info_parameter(): - expected_archive = 'archive-name' - flexmock(module.environment).should_receive('make_environment') - flexmock(module).should_receive('execute_command').with_args( - ('borg', 'list', '--info') + BORG_LIST_LATEST_ARGUMENTS, - output_log_level=None, - borg_local_path='borg', - extra_environment=None, - ).and_return(expected_archive + '\n') - insert_logging_mock(logging.INFO) - - assert module.resolve_archive_name('repo', 'latest', storage_config={}) == expected_archive - - -def test_resolve_archive_name_with_log_debug_calls_borg_with_debug_parameter(): - expected_archive = 'archive-name' - flexmock(module.environment).should_receive('make_environment') - flexmock(module).should_receive('execute_command').with_args( - ('borg', 'list', '--debug', '--show-rc') + BORG_LIST_LATEST_ARGUMENTS, - output_log_level=None, - borg_local_path='borg', - extra_environment=None, - ).and_return(expected_archive + '\n') - insert_logging_mock(logging.DEBUG) - - assert module.resolve_archive_name('repo', 'latest', storage_config={}) == expected_archive - - -def test_resolve_archive_name_with_local_path_calls_borg_via_local_path(): - expected_archive = 'archive-name' - flexmock(module.environment).should_receive('make_environment') - flexmock(module).should_receive('execute_command').with_args( - ('borg1', 'list') + BORG_LIST_LATEST_ARGUMENTS, - output_log_level=None, - borg_local_path='borg1', - extra_environment=None, - ).and_return(expected_archive + '\n') - - assert ( - module.resolve_archive_name('repo', 'latest', storage_config={}, local_path='borg1') - == expected_archive - ) - - -def test_resolve_archive_name_with_remote_path_calls_borg_with_remote_path_parameters(): - expected_archive = 'archive-name' - flexmock(module.environment).should_receive('make_environment') - flexmock(module).should_receive('execute_command').with_args( - ('borg', 'list', '--remote-path', 'borg1') + BORG_LIST_LATEST_ARGUMENTS, - output_log_level=None, - borg_local_path='borg', - extra_environment=None, - ).and_return(expected_archive + '\n') - - assert ( - module.resolve_archive_name('repo', 'latest', storage_config={}, remote_path='borg1') - == expected_archive - ) - - -def test_resolve_archive_name_without_archives_raises(): - flexmock(module.environment).should_receive('make_environment') - flexmock(module).should_receive('execute_command').with_args( - ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS, - output_log_level=None, - borg_local_path='borg', - extra_environment=None, - ).and_return('') - - with pytest.raises(ValueError): - module.resolve_archive_name('repo', 'latest', storage_config={}) - - -def test_resolve_archive_name_with_lock_wait_calls_borg_with_lock_wait_parameters(): - expected_archive = 'archive-name' - - flexmock(module.environment).should_receive('make_environment') - flexmock(module).should_receive('execute_command').with_args( - ('borg', 'list', '--lock-wait', 'okay') + BORG_LIST_LATEST_ARGUMENTS, - output_log_level=None, - borg_local_path='borg', - extra_environment=None, - ).and_return(expected_archive + '\n') - - assert ( - module.resolve_archive_name('repo', 'latest', storage_config={'lock_wait': 'okay'}) - == expected_archive - ) - def test_make_list_command_includes_log_info(): insert_logging_mock(logging.INFO) + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) command = module.make_list_command( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=flexmock(archive=None, paths=None, json=False), ) @@ -139,10 +27,14 @@ def test_make_list_command_includes_log_info(): def test_make_list_command_includes_json_but_not_info(): insert_logging_mock(logging.INFO) + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',)) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) command = module.make_list_command( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=flexmock(archive=None, paths=None, json=True), ) @@ -151,10 +43,14 @@ def test_make_list_command_includes_json_but_not_info(): def test_make_list_command_includes_log_debug(): insert_logging_mock(logging.DEBUG) + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) command = module.make_list_command( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=flexmock(archive=None, paths=None, json=False), ) @@ -163,10 +59,14 @@ def test_make_list_command_includes_log_debug(): def test_make_list_command_includes_json_but_not_debug(): insert_logging_mock(logging.DEBUG) + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',)) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) command = module.make_list_command( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=flexmock(archive=None, paths=None, json=True), ) @@ -174,9 +74,14 @@ def test_make_list_command_includes_json_but_not_debug(): def test_make_list_command_includes_json(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',)) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + command = module.make_list_command( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=flexmock(archive=None, paths=None, json=True), ) @@ -184,9 +89,16 @@ def test_make_list_command_includes_json(): def test_make_list_command_includes_lock_wait(): + flexmock(module.flags).should_receive('make_flags').and_return(()).and_return( + ('--lock-wait', '5') + ) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + command = module.make_list_command( repository='repo', storage_config={'lock_wait': 5}, + local_borg_version='1.2.3', list_arguments=flexmock(archive=None, paths=None, json=False), ) @@ -194,9 +106,16 @@ def test_make_list_command_includes_lock_wait(): def test_make_list_command_includes_archive(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) + command = module.make_list_command( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=flexmock(archive='archive', paths=None, json=False), ) @@ -204,9 +123,16 @@ def test_make_list_command_includes_archive(): def test_make_list_command_includes_archive_and_path(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) + command = module.make_list_command( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=flexmock(archive='archive', paths=['var/lib'], json=False), ) @@ -214,9 +140,14 @@ def test_make_list_command_includes_archive_and_path(): def test_make_list_command_includes_local_path(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + command = module.make_list_command( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=flexmock(archive=None, paths=None, json=False), local_path='borg2', ) @@ -225,9 +156,16 @@ def test_make_list_command_includes_local_path(): def test_make_list_command_includes_remote_path(): + flexmock(module.flags).should_receive('make_flags').and_return( + ('--remote-path', 'borg2') + ).and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + command = module.make_list_command( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=flexmock(archive=None, paths=None, json=False), remote_path='borg2', ) @@ -236,9 +174,14 @@ def test_make_list_command_includes_remote_path(): def test_make_list_command_includes_short(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--short',)) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + command = module.make_list_command( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=flexmock(archive=None, paths=None, json=False, short=True), ) @@ -260,16 +203,23 @@ def test_make_list_command_includes_short(): ), ) def test_make_list_command_includes_additional_flags(argument_name): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return( + (f"--{argument_name.replace('_', '-')}", 'value') + ) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + command = module.make_list_command( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=flexmock( archive=None, paths=None, json=False, find_paths=None, format=None, - **{argument_name: 'value'} + **{argument_name: 'value'}, ), ) @@ -303,89 +253,109 @@ def test_make_find_paths_adds_globs_to_path_fragments(): assert module.make_find_paths(('foo.txt',)) == ('sh:**/*foo.txt*/**',) -def test_list_archives_calls_borg_with_parameters(): - list_arguments = argparse.Namespace(archive=None, paths=None, json=False, find_paths=None) +def test_list_archive_calls_borg_with_parameters(): + list_arguments = argparse.Namespace(archive='archive', paths=None, json=False, find_paths=None) + flexmock(module.feature).should_receive('available').and_return(False) flexmock(module).should_receive('make_list_command').with_args( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=list_arguments, local_path='borg', remote_path=None, - ).and_return(('borg', 'list', 'repo')) + ).and_return(('borg', 'list', 'repo::archive')) flexmock(module).should_receive('make_find_paths').and_return(()) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'list', 'repo'), + ('borg', 'list', 'repo::archive'), output_log_level=logging.WARNING, borg_local_path='borg', extra_environment=None, ).once() - module.list_archives( - repository='repo', storage_config={}, list_arguments=list_arguments, + module.list_archive( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=list_arguments, ) -def test_list_archives_with_json_suppresses_most_borg_output(): - list_arguments = argparse.Namespace(archive=None, paths=None, json=True, find_paths=None) +def test_list_archive_with_json_suppresses_most_borg_output(): + list_arguments = argparse.Namespace(archive='archive', paths=None, json=True, find_paths=None) + flexmock(module.feature).should_receive('available').and_return(False) flexmock(module).should_receive('make_list_command').with_args( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=list_arguments, local_path='borg', remote_path=None, - ).and_return(('borg', 'list', 'repo')) + ).and_return(('borg', 'list', 'repo::archive')) flexmock(module).should_receive('make_find_paths').and_return(()) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'list', 'repo'), + ('borg', 'list', 'repo::archive'), output_log_level=None, borg_local_path='borg', extra_environment=None, ).once() - module.list_archives( - repository='repo', storage_config={}, list_arguments=list_arguments, + module.list_archive( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=list_arguments, ) -def test_list_archives_calls_borg_with_local_path(): - list_arguments = argparse.Namespace(archive=None, paths=None, json=False, find_paths=None) +def test_list_archive_calls_borg_with_local_path(): + list_arguments = argparse.Namespace(archive='archive', paths=None, json=False, find_paths=None) + flexmock(module.feature).should_receive('available').and_return(False) flexmock(module).should_receive('make_list_command').with_args( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=list_arguments, local_path='borg2', remote_path=None, - ).and_return(('borg2', 'list', 'repo')) + ).and_return(('borg2', 'list', 'repo::archive')) flexmock(module).should_receive('make_find_paths').and_return(()) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg2', 'list', 'repo'), + ('borg2', 'list', 'repo::archive'), output_log_level=logging.WARNING, borg_local_path='borg2', extra_environment=None, ).once() - module.list_archives( - repository='repo', storage_config={}, list_arguments=list_arguments, local_path='borg2', + module.list_archive( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=list_arguments, + local_path='borg2', ) -def test_list_archives_calls_borg_multiple_times_with_find_paths(): +def test_list_archive_calls_borg_multiple_times_with_find_paths(): glob_paths = ('**/*foo.txt*/**',) list_arguments = argparse.Namespace( - archive=None, paths=None, json=False, find_paths=['foo.txt'], format=None + archive=None, + json=False, + find_paths=['foo.txt'], + prefix=None, + glob_archives=None, + sort_by=None, + first=None, + last=None, ) - flexmock(module).should_receive('make_list_command').and_return( - ('borg', 'list', 'repo') - ).and_return(('borg', 'list', 'repo::archive1')).and_return(('borg', 'list', 'repo::archive2')) - flexmock(module).should_receive('make_find_paths').and_return(glob_paths) - flexmock(module.environment).should_receive('make_environment') + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.rlist).should_receive('make_rlist_command').and_return(('borg', 'list', 'repo')) flexmock(module).should_receive('execute_command').with_args( ('borg', 'list', 'repo'), output_log_level=None, @@ -394,6 +364,10 @@ def test_list_archives_calls_borg_multiple_times_with_find_paths(): ).and_return( 'archive1 Sun, 2022-05-29 15:27:04 [abc]\narchive2 Mon, 2022-05-30 19:47:15 [xyz]' ).once() + flexmock(module).should_receive('make_list_command').and_return( + ('borg', 'list', 'repo::archive1') + ).and_return(('borg', 'list', 'repo::archive2')) + flexmock(module).should_receive('make_find_paths').and_return(glob_paths) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'list', 'repo::archive1') + glob_paths, @@ -408,17 +382,22 @@ def test_list_archives_calls_borg_multiple_times_with_find_paths(): extra_environment=None, ).once() - module.list_archives( - repository='repo', storage_config={}, list_arguments=list_arguments, + module.list_archive( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=list_arguments, ) -def test_list_archives_calls_borg_with_archive(): +def test_list_archive_calls_borg_with_archive(): list_arguments = argparse.Namespace(archive='archive', paths=None, json=False, find_paths=None) + flexmock(module.feature).should_receive('available').and_return(False) flexmock(module).should_receive('make_list_command').with_args( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=list_arguments, local_path='borg', remote_path=None, @@ -432,6 +411,124 @@ def test_list_archives_calls_borg_with_archive(): extra_environment=None, ).once() - module.list_archives( - repository='repo', storage_config={}, list_arguments=list_arguments, + module.list_archive( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=list_arguments, + ) + + +def test_list_archive_without_archive_delegates_to_list_repository(): + list_arguments = argparse.Namespace( + archive=None, + short=None, + format=None, + json=None, + prefix=None, + glob_archives=None, + sort_by=None, + first=None, + last=None, + find_paths=None, + ) + + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.rlist).should_receive('list_repository') + flexmock(module.environment).should_receive('make_environment').never() + flexmock(module).should_receive('execute_command').never() + + module.list_archive( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=list_arguments, + ) + + +def test_list_archive_with_borg_features_without_archive_delegates_to_list_repository(): + list_arguments = argparse.Namespace( + archive=None, + short=None, + format=None, + json=None, + prefix=None, + glob_archives=None, + sort_by=None, + first=None, + last=None, + find_paths=None, + ) + + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.rlist).should_receive('list_repository') + flexmock(module.environment).should_receive('make_environment').never() + flexmock(module).should_receive('execute_command').never() + + module.list_archive( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=list_arguments, + ) + + +@pytest.mark.parametrize( + 'archive_filter_flag', ('prefix', 'glob_archives', 'sort_by', 'first', 'last',), +) +def test_list_archive_with_archive_disallows_archive_filter_flag_if_rlist_feature_available( + archive_filter_flag, +): + list_arguments = argparse.Namespace( + archive='archive', paths=None, json=False, find_paths=None, **{archive_filter_flag: 'foo'} + ) + + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.RLIST, '1.2.3' + ).and_return(True) + + with pytest.raises(ValueError): + module.list_archive( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=list_arguments, + ) + + +@pytest.mark.parametrize( + 'archive_filter_flag', ('prefix', 'glob_archives', 'sort_by', 'first', 'last',), +) +def test_list_archive_with_archive_allows_archive_filter_flag_if_rlist_feature_unavailable( + archive_filter_flag, +): + list_arguments = argparse.Namespace( + archive='archive', paths=None, json=False, find_paths=None, **{archive_filter_flag: 'foo'} + ) + + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.RLIST, '1.2.3' + ).and_return(False) + flexmock(module).should_receive('make_list_command').with_args( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=list_arguments, + local_path='borg', + remote_path=None, + ).and_return(('borg', 'list', 'repo::archive')) + flexmock(module).should_receive('make_find_paths').and_return(()) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'list', 'repo::archive'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ).once() + + module.list_archive( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=list_arguments, ) diff --git a/tests/unit/borg/test_rlist.py b/tests/unit/borg/test_rlist.py new file mode 100644 index 000000000..816843551 --- /dev/null +++ b/tests/unit/borg/test_rlist.py @@ -0,0 +1,381 @@ +import argparse +import logging + +import pytest +from flexmock import flexmock + +from borgmatic.borg import rlist as module + +from ..test_verbosity import insert_logging_mock + +BORG_LIST_LATEST_ARGUMENTS = ( + '--last', + '1', + '--short', + 'repo', +) + + +def test_resolve_archive_name_passes_through_non_latest_archive_name(): + archive = 'myhost-2030-01-01T14:41:17.647620' + + assert ( + module.resolve_archive_name('repo', archive, storage_config={}, local_borg_version='1.2.3') + == archive + ) + + +def test_resolve_archive_name_calls_borg_with_parameters(): + expected_archive = 'archive-name' + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS, + output_log_level=None, + borg_local_path='borg', + extra_environment=None, + ).and_return(expected_archive + '\n') + + assert ( + module.resolve_archive_name('repo', 'latest', storage_config={}, local_borg_version='1.2.3') + == expected_archive + ) + + +def test_resolve_archive_name_with_log_info_calls_borg_with_info_parameter(): + expected_archive = 'archive-name' + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'list', '--info') + BORG_LIST_LATEST_ARGUMENTS, + output_log_level=None, + borg_local_path='borg', + extra_environment=None, + ).and_return(expected_archive + '\n') + insert_logging_mock(logging.INFO) + + assert ( + module.resolve_archive_name('repo', 'latest', storage_config={}, local_borg_version='1.2.3') + == expected_archive + ) + + +def test_resolve_archive_name_with_log_debug_calls_borg_with_debug_parameter(): + expected_archive = 'archive-name' + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'list', '--debug', '--show-rc') + BORG_LIST_LATEST_ARGUMENTS, + output_log_level=None, + borg_local_path='borg', + extra_environment=None, + ).and_return(expected_archive + '\n') + insert_logging_mock(logging.DEBUG) + + assert ( + module.resolve_archive_name('repo', 'latest', storage_config={}, local_borg_version='1.2.3') + == expected_archive + ) + + +def test_resolve_archive_name_with_local_path_calls_borg_via_local_path(): + expected_archive = 'archive-name' + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg1', 'list') + BORG_LIST_LATEST_ARGUMENTS, + output_log_level=None, + borg_local_path='borg1', + extra_environment=None, + ).and_return(expected_archive + '\n') + + assert ( + module.resolve_archive_name( + 'repo', 'latest', storage_config={}, local_borg_version='1.2.3', local_path='borg1' + ) + == expected_archive + ) + + +def test_resolve_archive_name_with_remote_path_calls_borg_with_remote_path_parameters(): + expected_archive = 'archive-name' + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'list', '--remote-path', 'borg1') + BORG_LIST_LATEST_ARGUMENTS, + output_log_level=None, + borg_local_path='borg', + extra_environment=None, + ).and_return(expected_archive + '\n') + + assert ( + module.resolve_archive_name( + 'repo', 'latest', storage_config={}, local_borg_version='1.2.3', remote_path='borg1' + ) + == expected_archive + ) + + +def test_resolve_archive_name_without_archives_raises(): + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS, + output_log_level=None, + borg_local_path='borg', + extra_environment=None, + ).and_return('') + + with pytest.raises(ValueError): + module.resolve_archive_name('repo', 'latest', storage_config={}, local_borg_version='1.2.3') + + +def test_resolve_archive_name_with_lock_wait_calls_borg_with_lock_wait_parameters(): + expected_archive = 'archive-name' + + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'list', '--lock-wait', 'okay') + BORG_LIST_LATEST_ARGUMENTS, + output_log_level=None, + borg_local_path='borg', + extra_environment=None, + ).and_return(expected_archive + '\n') + + assert ( + module.resolve_archive_name( + 'repo', 'latest', storage_config={'lock_wait': 'okay'}, local_borg_version='1.2.3' + ) + == expected_archive + ) + + +def test_make_rlist_command_includes_log_info(): + insert_logging_mock(logging.INFO) + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + + command = module.make_rlist_command( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=flexmock(archive=None, paths=None, json=False), + ) + + assert command == ('borg', 'list', '--info', 'repo') + + +def test_make_rlist_command_includes_json_but_not_info(): + insert_logging_mock(logging.INFO) + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',)) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + + command = module.make_rlist_command( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=flexmock(archive=None, paths=None, json=True), + ) + + assert command == ('borg', 'list', '--json', 'repo') + + +def test_make_rlist_command_includes_log_debug(): + insert_logging_mock(logging.DEBUG) + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + + command = module.make_rlist_command( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=flexmock(archive=None, paths=None, json=False), + ) + + assert command == ('borg', 'list', '--debug', '--show-rc', 'repo') + + +def test_make_rlist_command_includes_json_but_not_debug(): + insert_logging_mock(logging.DEBUG) + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',)) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + + command = module.make_rlist_command( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=flexmock(archive=None, paths=None, json=True), + ) + + assert command == ('borg', 'list', '--json', 'repo') + + +def test_make_rlist_command_includes_json(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',)) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + + command = module.make_rlist_command( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=flexmock(archive=None, paths=None, json=True), + ) + + assert command == ('borg', 'list', '--json', 'repo') + + +def test_make_rlist_command_includes_lock_wait(): + flexmock(module.flags).should_receive('make_flags').and_return(()).and_return( + ('--lock-wait', '5') + ) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + + command = module.make_rlist_command( + repository='repo', + storage_config={'lock_wait': 5}, + local_borg_version='1.2.3', + rlist_arguments=flexmock(archive=None, paths=None, json=False), + ) + + assert command == ('borg', 'list', '--lock-wait', '5', 'repo') + + +def test_make_rlist_command_includes_local_path(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + + command = module.make_rlist_command( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=flexmock(archive=None, paths=None, json=False), + local_path='borg2', + ) + + assert command == ('borg2', 'list', 'repo') + + +def test_make_rlist_command_includes_remote_path(): + flexmock(module.flags).should_receive('make_flags').and_return( + ('--remote-path', 'borg2') + ).and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + + command = module.make_rlist_command( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=flexmock(archive=None, paths=None, json=False), + remote_path='borg2', + ) + + assert command == ('borg', 'list', '--remote-path', 'borg2', 'repo') + + +def test_make_rlist_command_includes_short(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--short',)) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + + command = module.make_rlist_command( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=flexmock(archive=None, paths=None, json=False, short=True), + ) + + assert command == ('borg', 'list', '--short', 'repo') + + +@pytest.mark.parametrize( + 'argument_name', + ( + 'prefix', + 'glob_archives', + 'sort_by', + 'first', + 'last', + 'exclude', + 'exclude_from', + 'pattern', + 'patterns_from', + ), +) +def test_make_rlist_command_includes_additional_flags(argument_name): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return( + (f"--{argument_name.replace('_', '-')}", 'value') + ) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + + command = module.make_rlist_command( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=flexmock( + archive=None, + paths=None, + json=False, + find_paths=None, + format=None, + **{argument_name: 'value'}, + ), + ) + + assert command == ('borg', 'list', '--' + argument_name.replace('_', '-'), 'value', 'repo') + + +def test_list_repository_calls_borg_with_parameters(): + rlist_arguments = argparse.Namespace(json=False) + + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module).should_receive('make_rlist_command').with_args( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=rlist_arguments, + local_path='borg', + remote_path=None, + ).and_return(('borg', 'rlist', 'repo')) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'rlist', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ).once() + + module.list_repository( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=rlist_arguments, + ) + + +def test_list_repository_with_json_returns_borg_output(): + rlist_arguments = argparse.Namespace(json=True) + json_output = flexmock() + + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module).should_receive('make_rlist_command').with_args( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=rlist_arguments, + local_path='borg', + remote_path=None, + ).and_return(('borg', 'rlist', 'repo')) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').and_return(json_output) + + assert ( + module.list_repository( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=rlist_arguments, + ) + == json_output + ) diff --git a/tests/unit/commands/test_borgmatic.py b/tests/unit/commands/test_borgmatic.py index e17c20ef9..365400c50 100644 --- a/tests/unit/commands/test_borgmatic.py +++ b/tests/unit/commands/test_borgmatic.py @@ -571,10 +571,35 @@ def test_run_actions_does_not_raise_for_mount_action(): ) +def test_run_actions_does_not_raise_for_rlist_action(): + flexmock(module.validate).should_receive('repositories_match').and_return(True) + flexmock(module.borg_rlist).should_receive('list_repository') + arguments = { + 'global': flexmock(monitoring_verbosity=1, dry_run=False), + 'rlist': flexmock(repository=flexmock(), json=flexmock()), + } + + list( + module.run_actions( + arguments=arguments, + config_filename='test.yaml', + location={'repositories': ['repo']}, + storage={}, + retention={}, + consistency={}, + hooks={}, + local_path=None, + remote_path=None, + local_borg_version=None, + repository_path='repo', + ) + ) + + def test_run_actions_does_not_raise_for_list_action(): flexmock(module.validate).should_receive('repositories_match').and_return(True) - flexmock(module.borg_list).should_receive('resolve_archive_name').and_return(flexmock()) - flexmock(module.borg_list).should_receive('list_archives') + flexmock(module.borg_rlist).should_receive('resolve_archive_name').and_return(flexmock()) + flexmock(module.borg_list).should_receive('list_archive') arguments = { 'global': flexmock(monitoring_verbosity=1, dry_run=False), 'list': flexmock(repository=flexmock(), archive=flexmock(), json=flexmock()), @@ -624,7 +649,7 @@ def test_run_actions_does_not_raise_for_rinfo_action(): def test_run_actions_does_not_raise_for_info_action(): flexmock(module.validate).should_receive('repositories_match').and_return(True) - flexmock(module.borg_list).should_receive('resolve_archive_name').and_return(flexmock()) + flexmock(module.borg_rlist).should_receive('resolve_archive_name').and_return(flexmock()) flexmock(module.borg_info).should_receive('display_archives_info') arguments = { 'global': flexmock(monitoring_verbosity=1, dry_run=False), @@ -650,7 +675,7 @@ def test_run_actions_does_not_raise_for_info_action(): def test_run_actions_does_not_raise_for_borg_action(): flexmock(module.validate).should_receive('repositories_match').and_return(True) - flexmock(module.borg_list).should_receive('resolve_archive_name').and_return(flexmock()) + flexmock(module.borg_rlist).should_receive('resolve_archive_name').and_return(flexmock()) flexmock(module.borg_borg).should_receive('run_arbitrary_borg') arguments = { 'global': flexmock(monitoring_verbosity=1, dry_run=False), From 7626fe11899454159f1d3e7e95c00f7b6c8e702e Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Mon, 15 Aug 2022 15:40:28 -0700 Subject: [PATCH 18/38] Disallow borg list --json with --archive or --find (#557). --- borgmatic/borg/list.py | 14 ++++++++------ borgmatic/commands/arguments.py | 2 +- tests/unit/borg/test_list.py | 31 ++++++++----------------------- 3 files changed, 17 insertions(+), 30 deletions(-) diff --git a/borgmatic/borg/list.py b/borgmatic/borg/list.py index fadc5055d..cbca8d7c7 100644 --- a/borgmatic/borg/list.py +++ b/borgmatic/borg/list.py @@ -117,9 +117,14 @@ def list_archive( for flag_name in ('prefix', 'glob-archives', 'sort-by', 'first', 'last'): if getattr(list_arguments, flag_name.replace('-', '_'), None): raise ValueError( - f'The --{flag_name} flag on the list action is not supported when using the --archive flag and Borg 2.x.' + f'The --{flag_name} flag on the list action is not supported when using the --archive/--find flags and Borg 2.x.' ) + if list_arguments.json: + raise ValueError( + 'The --json flag on the list action is not supported when using the --archive/--find flags.' + ) + borg_environment = environment.make_environment(storage_config) # If there are any paths to find (and there's not a single archive already selected), start by @@ -173,12 +178,9 @@ def list_archive( remote_path, ) + make_find_paths(list_arguments.find_paths) - output = execute_command( + execute_command( main_command, - output_log_level=None if list_arguments.json else logging.WARNING, + output_log_level=logging.WARNING, borg_local_path=local_path, extra_environment=borg_environment, ) - - if list_arguments.json: - return output diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index 02180def7..8a2ed346f 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -626,7 +626,7 @@ def make_parsers(): '--successful', default=True, action='store_true', - help='Deprecated; no effect. Newer versions of Borg list successful (non-checkpoint) archives by default.', + 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' diff --git a/tests/unit/borg/test_list.py b/tests/unit/borg/test_list.py index 7111cb61a..d1c5d397b 100644 --- a/tests/unit/borg/test_list.py +++ b/tests/unit/borg/test_list.py @@ -282,33 +282,18 @@ def test_list_archive_calls_borg_with_parameters(): ) -def test_list_archive_with_json_suppresses_most_borg_output(): +def test_list_archive_with_archive_and_json_errors(): list_arguments = argparse.Namespace(archive='archive', paths=None, json=True, find_paths=None) flexmock(module.feature).should_receive('available').and_return(False) - flexmock(module).should_receive('make_list_command').with_args( - repository='repo', - storage_config={}, - local_borg_version='1.2.3', - list_arguments=list_arguments, - local_path='borg', - remote_path=None, - ).and_return(('borg', 'list', 'repo::archive')) - flexmock(module).should_receive('make_find_paths').and_return(()) - flexmock(module.environment).should_receive('make_environment') - flexmock(module).should_receive('execute_command').with_args( - ('borg', 'list', 'repo::archive'), - output_log_level=None, - borg_local_path='borg', - extra_environment=None, - ).once() - module.list_archive( - repository='repo', - storage_config={}, - local_borg_version='1.2.3', - list_arguments=list_arguments, - ) + with pytest.raises(ValueError): + module.list_archive( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=list_arguments, + ) def test_list_archive_calls_borg_with_local_path(): From d807ce095ec4c8d1cf30072fd75646c75184fd0c Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Mon, 15 Aug 2022 17:34:12 -0700 Subject: [PATCH 19/38] Update export-tar action for Borg 2 support (#557). --- NEWS | 9 +++--- borgmatic/borg/export_tar.py | 17 ++++++---- borgmatic/commands/borgmatic.py | 1 + tests/unit/borg/test_export_tar.py | 52 ++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 10 deletions(-) diff --git a/NEWS b/NEWS index a996306c3..b753753b2 100644 --- a/NEWS +++ b/NEWS @@ -1,8 +1,9 @@ 1.7.0.dev0 - * #557: Support for Borg 2 while still working with Borg 1. If you install Borg 2, you'll need to - manually "borg transfer" or "borgmatic transfer" any existing Borg 1 repositories before use. See - the Borg 2.0 changelog summary for more information about Borg 2: - https://www.borgbackup.org/releases/borg-2.0.html + * #557: Support for Borg 2 while still working with Borg 1. This includes new borgmatic actions + like "rcreate" (replaces "init"), "rlist" (list archives in repository), and "rinfo" (show + repository info). If you install Borg 2, you'll need to manually "borg transfer" or "borgmatic + transfer" your existing Borg 1 repositories before use. See the Borg 2.0 changelog for more + information about Borg 2: https://www.borgbackup.org/releases/borg-2.0.html * #565: Fix handling of "repository" and "data" consistency checks to prevent invalid Borg flags. * #566: Modify "mount" and "extract" actions to require the "--repository" flag when multiple repositories are configured. diff --git a/borgmatic/borg/export_tar.py b/borgmatic/borg/export_tar.py index 542092880..9bbd00823 100644 --- a/borgmatic/borg/export_tar.py +++ b/borgmatic/borg/export_tar.py @@ -1,7 +1,7 @@ import logging import os -from borgmatic.borg import environment +from borgmatic.borg import environment, flags from borgmatic.execute import DO_NOT_CAPTURE, execute_command logger = logging.getLogger(__name__) @@ -14,6 +14,7 @@ def export_tar_archive( paths, destination_path, storage_config, + local_borg_version, local_path='borg', remote_path=None, tar_filter=None, @@ -22,10 +23,10 @@ def export_tar_archive( ): ''' Given a dry-run flag, a local or remote repository path, an archive name, zero or more paths to - export from the archive, a destination path to export to, a storage configuration dict, optional - local and remote Borg paths, an optional filter program, whether to include per-file details, - and an optional number of path components to strip, export the archive into the given - destination path as a tar-formatted file. + export from the archive, a destination path to export to, a storage configuration dict, the + local Borg version, optional local and remote Borg paths, an optional filter program, whether to + include per-file details, and an optional number of path components to strip, export the archive + into the given destination path as a tar-formatted file. If the destination path is "-", then stream the output to stdout instead of to a file. ''' @@ -43,7 +44,11 @@ def export_tar_archive( + (('--dry-run',) if dry_run else ()) + (('--tar-filter', tar_filter) if tar_filter else ()) + (('--strip-components', str(strip_components)) if strip_components else ()) - + ('::'.join((repository if ':' in repository else os.path.abspath(repository), archive)),) + + flags.make_repository_archive_flags( + repository if ':' in repository else os.path.abspath(repository), + archive, + local_borg_version, + ) + (destination_path,) + (tuple(paths) if paths else ()) ) diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index e4455daa9..c526b102b 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -484,6 +484,7 @@ def run_actions( arguments['export-tar'].paths, arguments['export-tar'].destination, storage, + local_borg_version, local_path=local_path, remote_path=remote_path, tar_filter=arguments['export-tar'].tar_filter, diff --git a/tests/unit/borg/test_export_tar.py b/tests/unit/borg/test_export_tar.py index 11b2b20af..f8d56372b 100644 --- a/tests/unit/borg/test_export_tar.py +++ b/tests/unit/borg/test_export_tar.py @@ -21,6 +21,9 @@ def insert_execute_command_mock( def test_export_tar_archive_calls_borg_with_path_parameters(): + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) flexmock(module.os.path).should_receive('abspath').and_return('repo') insert_execute_command_mock( ('borg', 'export-tar', 'repo::archive', 'test.tar', 'path1', 'path2') @@ -33,10 +36,14 @@ def test_export_tar_archive_calls_borg_with_path_parameters(): paths=['path1', 'path2'], destination_path='test.tar', storage_config={}, + local_borg_version='1.2.3', ) def test_export_tar_archive_calls_borg_with_local_path_parameters(): + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) flexmock(module.os.path).should_receive('abspath').and_return('repo') insert_execute_command_mock( ('borg1', 'export-tar', 'repo::archive', 'test.tar'), borg_local_path='borg1' @@ -49,11 +56,15 @@ def test_export_tar_archive_calls_borg_with_local_path_parameters(): paths=None, destination_path='test.tar', storage_config={}, + local_borg_version='1.2.3', local_path='borg1', ) def test_export_tar_archive_calls_borg_with_remote_path_parameters(): + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) flexmock(module.os.path).should_receive('abspath').and_return('repo') insert_execute_command_mock( ('borg', 'export-tar', '--remote-path', 'borg1', 'repo::archive', 'test.tar') @@ -66,11 +77,15 @@ def test_export_tar_archive_calls_borg_with_remote_path_parameters(): paths=None, destination_path='test.tar', storage_config={}, + local_borg_version='1.2.3', remote_path='borg1', ) def test_export_tar_archive_calls_borg_with_umask_parameters(): + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) flexmock(module.os.path).should_receive('abspath').and_return('repo') insert_execute_command_mock( ('borg', 'export-tar', '--umask', '0770', 'repo::archive', 'test.tar') @@ -83,10 +98,14 @@ def test_export_tar_archive_calls_borg_with_umask_parameters(): paths=None, destination_path='test.tar', storage_config={'umask': '0770'}, + local_borg_version='1.2.3', ) def test_export_tar_archive_calls_borg_with_lock_wait_parameters(): + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) flexmock(module.os.path).should_receive('abspath').and_return('repo') insert_execute_command_mock( ('borg', 'export-tar', '--lock-wait', '5', 'repo::archive', 'test.tar') @@ -99,10 +118,14 @@ def test_export_tar_archive_calls_borg_with_lock_wait_parameters(): paths=None, destination_path='test.tar', storage_config={'lock_wait': '5'}, + local_borg_version='1.2.3', ) def test_export_tar_archive_with_log_info_calls_borg_with_info_parameter(): + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) flexmock(module.os.path).should_receive('abspath').and_return('repo') insert_execute_command_mock(('borg', 'export-tar', '--info', 'repo::archive', 'test.tar')) insert_logging_mock(logging.INFO) @@ -114,10 +137,14 @@ def test_export_tar_archive_with_log_info_calls_borg_with_info_parameter(): paths=None, destination_path='test.tar', storage_config={}, + local_borg_version='1.2.3', ) def test_export_tar_archive_with_log_debug_calls_borg_with_debug_parameters(): + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) flexmock(module.os.path).should_receive('abspath').and_return('repo') insert_execute_command_mock( ('borg', 'export-tar', '--debug', '--show-rc', 'repo::archive', 'test.tar') @@ -131,10 +158,14 @@ def test_export_tar_archive_with_log_debug_calls_borg_with_debug_parameters(): paths=None, destination_path='test.tar', storage_config={}, + local_borg_version='1.2.3', ) def test_export_tar_archive_calls_borg_with_dry_run_parameter(): + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) flexmock(module.os.path).should_receive('abspath').and_return('repo') flexmock(module).should_receive('execute_command').never() @@ -145,10 +176,14 @@ def test_export_tar_archive_calls_borg_with_dry_run_parameter(): paths=None, destination_path='test.tar', storage_config={}, + local_borg_version='1.2.3', ) def test_export_tar_archive_calls_borg_with_tar_filter_parameters(): + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) flexmock(module.os.path).should_receive('abspath').and_return('repo') insert_execute_command_mock( ('borg', 'export-tar', '--tar-filter', 'bzip2', 'repo::archive', 'test.tar') @@ -161,11 +196,15 @@ def test_export_tar_archive_calls_borg_with_tar_filter_parameters(): paths=None, destination_path='test.tar', storage_config={}, + local_borg_version='1.2.3', tar_filter='bzip2', ) def test_export_tar_archive_calls_borg_with_list_parameter(): + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) flexmock(module.os.path).should_receive('abspath').and_return('repo') insert_execute_command_mock( ('borg', 'export-tar', '--list', 'repo::archive', 'test.tar'), @@ -179,11 +218,15 @@ def test_export_tar_archive_calls_borg_with_list_parameter(): paths=None, destination_path='test.tar', storage_config={}, + local_borg_version='1.2.3', files=True, ) def test_export_tar_archive_calls_borg_with_strip_components_parameter(): + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) flexmock(module.os.path).should_receive('abspath').and_return('repo') insert_execute_command_mock( ('borg', 'export-tar', '--strip-components', '5', 'repo::archive', 'test.tar') @@ -196,11 +239,15 @@ def test_export_tar_archive_calls_borg_with_strip_components_parameter(): paths=None, destination_path='test.tar', storage_config={}, + local_borg_version='1.2.3', strip_components=5, ) def test_export_tar_archive_skips_abspath_for_remote_repository_parameter(): + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('server:repo::archive',) + ) flexmock(module.os.path).should_receive('abspath').never() insert_execute_command_mock(('borg', 'export-tar', 'server:repo::archive', 'test.tar')) @@ -211,10 +258,14 @@ def test_export_tar_archive_skips_abspath_for_remote_repository_parameter(): paths=None, destination_path='test.tar', storage_config={}, + local_borg_version='1.2.3', ) def test_export_tar_archive_calls_borg_with_stdout_destination_path(): + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) flexmock(module.os.path).should_receive('abspath').and_return('repo') insert_execute_command_mock(('borg', 'export-tar', 'repo::archive', '-'), capture=False) @@ -225,4 +276,5 @@ def test_export_tar_archive_calls_borg_with_stdout_destination_path(): paths=None, destination_path='-', storage_config={}, + local_borg_version='1.2.3', ) From f36e38ec201d66971ffc0af61addcc0595124da7 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Mon, 15 Aug 2022 19:32:37 -0700 Subject: [PATCH 20/38] Update mount action for Borg 2 support (#557). --- borgmatic/borg/mount.py | 15 ++++++-- borgmatic/commands/borgmatic.py | 1 + tests/unit/borg/test_mount.py | 64 +++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/borgmatic/borg/mount.py b/borgmatic/borg/mount.py index 4f5db81a4..370ca13ca 100644 --- a/borgmatic/borg/mount.py +++ b/borgmatic/borg/mount.py @@ -1,6 +1,6 @@ import logging -from borgmatic.borg import environment +from borgmatic.borg import environment, feature, flags from borgmatic.execute import DO_NOT_CAPTURE, execute_command logger = logging.getLogger(__name__) @@ -14,13 +14,15 @@ def mount_archive( foreground, options, storage_config, + local_borg_version, local_path='borg', remote_path=None, ): ''' Given a local or remote repository path, an optional archive name, a filesystem mount point, zero or more paths to mount from the archive, extra Borg mount options, a storage configuration - dict, and optional local and remote Borg paths, mount the archive onto the mount point. + dict, the local Borg version, and optional local and remote Borg paths, mount the archive onto + the mount point. ''' umask = storage_config.get('umask', None) lock_wait = storage_config.get('lock_wait', None) @@ -34,7 +36,14 @@ def mount_archive( + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ()) + (('--foreground',) if foreground else ()) + (('-o', options) if options else ()) - + (('::'.join((repository, archive)),) if archive else (repository,)) + + ( + ( + flags.make_repository_flags(repository, local_borg_version) + + ('--glob-archives', archive) + ) + if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version) + else flags.make_repository_archive_flags(repository, archive, local_borg_version) + ) + (mount_point,) + (tuple(paths) if paths else ()) ) diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index c526b102b..777b39f0a 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -517,6 +517,7 @@ def run_actions( arguments['mount'].foreground, arguments['mount'].options, storage, + local_borg_version, local_path=local_path, remote_path=remote_path, ) diff --git a/tests/unit/borg/test_mount.py b/tests/unit/borg/test_mount.py index c5638caa9..d87a18c83 100644 --- a/tests/unit/borg/test_mount.py +++ b/tests/unit/borg/test_mount.py @@ -15,6 +15,10 @@ def insert_execute_command_mock(command): def test_mount_archive_calls_borg_with_required_parameters(): + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) insert_execute_command_mock(('borg', 'mount', 'repo::archive', '/mnt')) module.mount_archive( @@ -25,10 +29,34 @@ def test_mount_archive_calls_borg_with_required_parameters(): foreground=False, options=None, storage_config={}, + local_borg_version='1.2.3', + ) + + +def test_mount_archive_with_borg_features_calls_borg_with_repository_and_glob_archives_flags(): + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + insert_execute_command_mock( + ('borg', 'mount', '--repo', 'repo', '--glob-archives', 'archive', '/mnt') + ) + + module.mount_archive( + repository='repo', + archive='archive', + mount_point='/mnt', + paths=None, + foreground=False, + options=None, + storage_config={}, + local_borg_version='1.2.3', ) def test_mount_archive_calls_borg_with_path_parameters(): + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) insert_execute_command_mock(('borg', 'mount', 'repo::archive', '/mnt', 'path1', 'path2')) module.mount_archive( @@ -39,10 +67,15 @@ def test_mount_archive_calls_borg_with_path_parameters(): foreground=False, options=None, storage_config={}, + local_borg_version='1.2.3', ) def test_mount_archive_calls_borg_with_remote_path_parameters(): + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) insert_execute_command_mock( ('borg', 'mount', '--remote-path', 'borg1', 'repo::archive', '/mnt') ) @@ -55,11 +88,16 @@ def test_mount_archive_calls_borg_with_remote_path_parameters(): foreground=False, options=None, storage_config={}, + local_borg_version='1.2.3', remote_path='borg1', ) def test_mount_archive_calls_borg_with_umask_parameters(): + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) insert_execute_command_mock(('borg', 'mount', '--umask', '0770', 'repo::archive', '/mnt')) module.mount_archive( @@ -70,10 +108,15 @@ def test_mount_archive_calls_borg_with_umask_parameters(): foreground=False, options=None, storage_config={'umask': '0770'}, + local_borg_version='1.2.3', ) def test_mount_archive_calls_borg_with_lock_wait_parameters(): + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) insert_execute_command_mock(('borg', 'mount', '--lock-wait', '5', 'repo::archive', '/mnt')) module.mount_archive( @@ -84,10 +127,15 @@ def test_mount_archive_calls_borg_with_lock_wait_parameters(): foreground=False, options=None, storage_config={'lock_wait': '5'}, + local_borg_version='1.2.3', ) def test_mount_archive_with_log_info_calls_borg_with_info_parameter(): + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) insert_execute_command_mock(('borg', 'mount', '--info', 'repo::archive', '/mnt')) insert_logging_mock(logging.INFO) @@ -99,10 +147,15 @@ def test_mount_archive_with_log_info_calls_borg_with_info_parameter(): foreground=False, options=None, storage_config={}, + local_borg_version='1.2.3', ) def test_mount_archive_with_log_debug_calls_borg_with_debug_parameters(): + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) insert_execute_command_mock(('borg', 'mount', '--debug', '--show-rc', 'repo::archive', '/mnt')) insert_logging_mock(logging.DEBUG) @@ -114,10 +167,15 @@ def test_mount_archive_with_log_debug_calls_borg_with_debug_parameters(): foreground=False, options=None, storage_config={}, + local_borg_version='1.2.3', ) def test_mount_archive_calls_borg_with_foreground_parameter(): + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'mount', '--foreground', 'repo::archive', '/mnt'), @@ -134,10 +192,15 @@ def test_mount_archive_calls_borg_with_foreground_parameter(): foreground=True, options=None, storage_config={}, + local_borg_version='1.2.3', ) def test_mount_archive_calls_borg_with_options_parameters(): + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) insert_execute_command_mock(('borg', 'mount', '-o', 'super_mount', 'repo::archive', '/mnt')) module.mount_archive( @@ -148,4 +211,5 @@ def test_mount_archive_calls_borg_with_options_parameters(): foreground=False, options='super_mount', storage_config={}, + local_borg_version='1.2.3', ) From 30abd0e3deedbca640d18ba1b4fa446609424f4d Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Tue, 16 Aug 2022 09:30:00 -0700 Subject: [PATCH 21/38] Update borg action for Borg 2 support (#557). --- borgmatic/borg/borg.py | 39 ++++++++------ borgmatic/commands/borgmatic.py | 1 + tests/unit/borg/test_borg.py | 90 +++++++++++++++++++++++++++------ 3 files changed, 99 insertions(+), 31 deletions(-) diff --git a/borgmatic/borg/borg.py b/borgmatic/borg/borg.py index 24a88643f..991b736bf 100644 --- a/borgmatic/borg/borg.py +++ b/borgmatic/borg/borg.py @@ -1,24 +1,29 @@ import logging -from borgmatic.borg import environment -from borgmatic.borg.flags import make_flags +from borgmatic.borg import environment, flags from borgmatic.execute import execute_command logger = logging.getLogger(__name__) REPOSITORYLESS_BORG_COMMANDS = {'serve', None} -BORG_COMMANDS_WITH_SUBCOMMANDS = {'key', 'debug'} -BORG_SUBCOMMANDS_WITHOUT_REPOSITORY = (('debug', 'info'), ('debug', 'convert-profile')) +BORG_SUBCOMMANDS_WITH_SUBCOMMANDS = {'key', 'debug'} +BORG_SUBCOMMANDS_WITHOUT_REPOSITORY = (('debug', 'info'), ('debug', 'convert-profile'), ()) def run_arbitrary_borg( - repository, storage_config, options, archive=None, local_path='borg', remote_path=None + repository, + storage_config, + local_borg_version, + options, + archive=None, + local_path='borg', + remote_path=None, ): ''' - Given a local or remote repository path, a storage config dict, a sequence of arbitrary - command-line Borg options, and an optional archive name, run an arbitrary Borg command on the - given repository/archive. + Given a local or remote repository path, a storage config dict, the local Borg version, a + sequence of arbitrary command-line Borg options, and an optional archive name, run an arbitrary + Borg command on the given repository/archive. ''' lock_wait = storage_config.get('lock_wait', None) @@ -26,7 +31,7 @@ def run_arbitrary_borg( options = options[1:] if options[0] == '--' else options # Borg commands like "key" have a sub-command ("export", etc.) that must follow it. - command_options_start_index = 2 if options[0] in BORG_COMMANDS_WITH_SUBCOMMANDS else 1 + command_options_start_index = 2 if options[0] in BORG_SUBCOMMANDS_WITH_SUBCOMMANDS else 1 borg_command = tuple(options[:command_options_start_index]) command_options = tuple(options[command_options_start_index:]) except IndexError: @@ -34,21 +39,23 @@ def run_arbitrary_borg( command_options = () if borg_command in BORG_SUBCOMMANDS_WITHOUT_REPOSITORY: - repository_archive = None - else: - repository_archive = ( - '::'.join((repository, archive)) if repository and archive else repository + repository_archive_flags = () + elif archive: + repository_archive_flags = flags.make_repository_archive_flags( + repository, archive, local_borg_version ) + else: + repository_archive_flags = flags.make_repository_flags(repository, local_borg_version) full_command = ( (local_path,) + borg_command - + ((repository_archive,) if borg_command and repository_archive else ()) + + repository_archive_flags + command_options + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ()) + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ()) - + make_flags('remote-path', remote_path) - + make_flags('lock-wait', lock_wait) + + flags.make_flags('remote-path', remote_path) + + flags.make_flags('lock-wait', lock_wait) ) return execute_command( diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index 777b39f0a..60b71c8fd 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -722,6 +722,7 @@ def run_actions( borg_borg.run_arbitrary_borg( repository, storage, + local_borg_version, options=arguments['borg'].options, archive=archive_name, local_path=local_path, diff --git a/tests/unit/borg/test_borg.py b/tests/unit/borg/test_borg.py index 8282bd527..96e591566 100644 --- a/tests/unit/borg/test_borg.py +++ b/tests/unit/borg/test_borg.py @@ -8,6 +8,8 @@ from ..test_verbosity import insert_logging_mock def test_run_arbitrary_borg_calls_borg_with_parameters(): + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + flexmock(module.flags).should_receive('make_flags').and_return(()) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'break-lock', 'repo'), @@ -17,11 +19,13 @@ def test_run_arbitrary_borg_calls_borg_with_parameters(): ) module.run_arbitrary_borg( - repository='repo', storage_config={}, options=['break-lock'], + repository='repo', storage_config={}, local_borg_version='1.2.3', options=['break-lock'], ) def test_run_arbitrary_borg_with_log_info_calls_borg_with_info_parameter(): + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + flexmock(module.flags).should_receive('make_flags').and_return(()) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'break-lock', 'repo', '--info'), @@ -32,11 +36,13 @@ def test_run_arbitrary_borg_with_log_info_calls_borg_with_info_parameter(): insert_logging_mock(logging.INFO) module.run_arbitrary_borg( - repository='repo', storage_config={}, options=['break-lock'], + repository='repo', storage_config={}, local_borg_version='1.2.3', options=['break-lock'], ) def test_run_arbitrary_borg_with_log_debug_calls_borg_with_debug_parameter(): + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + flexmock(module.flags).should_receive('make_flags').and_return(()) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'break-lock', 'repo', '--debug', '--show-rc'), @@ -47,12 +53,16 @@ def test_run_arbitrary_borg_with_log_debug_calls_borg_with_debug_parameter(): insert_logging_mock(logging.DEBUG) module.run_arbitrary_borg( - repository='repo', storage_config={}, options=['break-lock'], + repository='repo', storage_config={}, local_borg_version='1.2.3', options=['break-lock'], ) def test_run_arbitrary_borg_with_lock_wait_calls_borg_with_lock_wait_parameters(): storage_config = {'lock_wait': 5} + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + flexmock(module.flags).should_receive('make_flags').and_return(()).and_return( + ('--lock-wait', '5') + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'break-lock', 'repo', '--lock-wait', '5'), @@ -62,12 +72,18 @@ def test_run_arbitrary_borg_with_lock_wait_calls_borg_with_lock_wait_parameters( ) module.run_arbitrary_borg( - repository='repo', storage_config=storage_config, options=['break-lock'], + repository='repo', + storage_config=storage_config, + local_borg_version='1.2.3', + options=['break-lock'], ) def test_run_arbitrary_borg_with_archive_calls_borg_with_archive_parameter(): - storage_config = {} + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) + flexmock(module.flags).should_receive('make_flags').and_return(()) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'break-lock', 'repo::archive'), @@ -77,11 +93,17 @@ def test_run_arbitrary_borg_with_archive_calls_borg_with_archive_parameter(): ) module.run_arbitrary_borg( - repository='repo', storage_config=storage_config, options=['break-lock'], archive='archive', + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + options=['break-lock'], + archive='archive', ) def test_run_arbitrary_borg_with_local_path_calls_borg_via_local_path(): + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + flexmock(module.flags).should_receive('make_flags').and_return(()) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg1', 'break-lock', 'repo'), @@ -91,11 +113,19 @@ def test_run_arbitrary_borg_with_local_path_calls_borg_via_local_path(): ) module.run_arbitrary_borg( - repository='repo', storage_config={}, options=['break-lock'], local_path='borg1', + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + options=['break-lock'], + local_path='borg1', ) def test_run_arbitrary_borg_with_remote_path_calls_borg_with_remote_path_parameters(): + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + flexmock(module.flags).should_receive('make_flags').and_return( + ('--remote-path', 'borg1') + ).and_return(()) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'break-lock', 'repo', '--remote-path', 'borg1'), @@ -105,11 +135,17 @@ def test_run_arbitrary_borg_with_remote_path_calls_borg_with_remote_path_paramet ) module.run_arbitrary_borg( - repository='repo', storage_config={}, options=['break-lock'], remote_path='borg1', + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + options=['break-lock'], + remote_path='borg1', ) def test_run_arbitrary_borg_passes_borg_specific_parameters_to_borg(): + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + flexmock(module.flags).should_receive('make_flags').and_return(()) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'list', 'repo', '--progress'), @@ -119,11 +155,16 @@ def test_run_arbitrary_borg_passes_borg_specific_parameters_to_borg(): ) module.run_arbitrary_borg( - repository='repo', storage_config={}, options=['list', '--progress'], + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + options=['list', '--progress'], ) def test_run_arbitrary_borg_omits_dash_dash_in_parameters_passed_to_borg(): + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + flexmock(module.flags).should_receive('make_flags').and_return(()) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'break-lock', 'repo'), @@ -133,22 +174,29 @@ def test_run_arbitrary_borg_omits_dash_dash_in_parameters_passed_to_borg(): ) module.run_arbitrary_borg( - repository='repo', storage_config={}, options=['--', 'break-lock'], + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + options=['--', 'break-lock'], ) def test_run_arbitrary_borg_without_borg_specific_parameters_does_not_raise(): + flexmock(module.flags).should_receive('make_repository_flags').never() + flexmock(module.flags).should_receive('make_flags').and_return(()) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg',), output_log_level=logging.WARNING, borg_local_path='borg', extra_environment=None, ) module.run_arbitrary_borg( - repository='repo', storage_config={}, options=[], + repository='repo', storage_config={}, local_borg_version='1.2.3', options=[], ) def test_run_arbitrary_borg_passes_key_sub_command_to_borg_before_repository(): + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + flexmock(module.flags).should_receive('make_flags').and_return(()) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'key', 'export', 'repo'), @@ -158,11 +206,13 @@ def test_run_arbitrary_borg_passes_key_sub_command_to_borg_before_repository(): ) module.run_arbitrary_borg( - repository='repo', storage_config={}, options=['key', 'export'], + repository='repo', storage_config={}, local_borg_version='1.2.3', options=['key', 'export'], ) def test_run_arbitrary_borg_passes_debug_sub_command_to_borg_before_repository(): + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + flexmock(module.flags).should_receive('make_flags').and_return(()) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'debug', 'dump-manifest', 'repo', 'path'), @@ -172,11 +222,16 @@ def test_run_arbitrary_borg_passes_debug_sub_command_to_borg_before_repository() ) module.run_arbitrary_borg( - repository='repo', storage_config={}, options=['debug', 'dump-manifest', 'path'], + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + options=['debug', 'dump-manifest', 'path'], ) def test_run_arbitrary_borg_with_debug_info_command_does_not_pass_borg_repository(): + flexmock(module.flags).should_receive('make_repository_flags').never() + flexmock(module.flags).should_receive('make_flags').and_return(()) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'debug', 'info'), @@ -186,11 +241,13 @@ def test_run_arbitrary_borg_with_debug_info_command_does_not_pass_borg_repositor ) module.run_arbitrary_borg( - repository='repo', storage_config={}, options=['debug', 'info'], + repository='repo', storage_config={}, local_borg_version='1.2.3', options=['debug', 'info'], ) def test_run_arbitrary_borg_with_debug_convert_profile_command_does_not_pass_borg_repository(): + flexmock(module.flags).should_receive('make_repository_flags').never() + flexmock(module.flags).should_receive('make_flags').and_return(()) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'debug', 'convert-profile', 'in', 'out'), @@ -200,5 +257,8 @@ def test_run_arbitrary_borg_with_debug_convert_profile_command_does_not_pass_bor ) module.run_arbitrary_borg( - repository='repo', storage_config={}, options=['debug', 'convert-profile', 'in', 'out'], + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + options=['debug', 'convert-profile', 'in', 'out'], ) From 2a1c6b1477710499da85154fd70bf515519350e3 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Tue, 16 Aug 2022 11:41:35 -0700 Subject: [PATCH 22/38] Update documentation with newly required ssh:// repository syntax for Borg 2 (#557). --- README.md | 4 ++-- .../backup-to-a-removable-drive-or-an-intermittent-server.md | 2 +- docs/how-to/make-backups-redundant.md | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 310d23af8..3d37f2c03 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ location: # Paths of local or remote repositories to backup to. repositories: - - 1234@usw-s001.rsync.net:backups.borg - - k8pDxu32@k8pDxu32.repo.borgbase.com:repo + - ssh://1234@usw-s001.rsync.net:backups.borg + - ssh://k8pDxu32@k8pDxu32.repo.borgbase.com:repo - /var/lib/backups/local.borg retention: diff --git a/docs/how-to/backup-to-a-removable-drive-or-an-intermittent-server.md b/docs/how-to/backup-to-a-removable-drive-or-an-intermittent-server.md index 1ce53bae4..d3b2e02bb 100644 --- a/docs/how-to/backup-to-a-removable-drive-or-an-intermittent-server.md +++ b/docs/how-to/backup-to-a-removable-drive-or-an-intermittent-server.md @@ -76,7 +76,7 @@ location: - /home repositories: - - me@buddys-server.org:backup.borg + - ssh://me@buddys-server.org:backup.borg hooks: before_backup: diff --git a/docs/how-to/make-backups-redundant.md b/docs/how-to/make-backups-redundant.md index c42e29ac9..7a5e5a36e 100644 --- a/docs/how-to/make-backups-redundant.md +++ b/docs/how-to/make-backups-redundant.md @@ -20,8 +20,8 @@ location: # Paths of local or remote repositories to backup to. repositories: - - 1234@usw-s001.rsync.net:backups.borg - - k8pDxu32@k8pDxu32.repo.borgbase.com:repo + - ssh://1234@usw-s001.rsync.net:backups.borg + - ssh://k8pDxu32@k8pDxu32.repo.borgbase.com:repo - /var/lib/backups/local.borg ``` From 28d847b8b1a8597f3002eb7a07134dc033806de7 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Wed, 17 Aug 2022 10:13:11 -0700 Subject: [PATCH 23/38] Warn and tranform on non-ssh://-style repositories (#557). --- README.md | 4 +- borgmatic/commands/borgmatic.py | 3 +- borgmatic/config/normalize.py | 47 +++++++++++++++++-- borgmatic/config/validate.py | 7 ++- ...movable-drive-or-an-intermittent-server.md | 2 +- docs/how-to/make-backups-redundant.md | 4 +- tests/integration/config/test_validate.py | 44 +++++++++-------- tests/unit/commands/test_borgmatic.py | 10 ++-- tests/unit/config/test_normalize.py | 47 +++++++++++++++++-- 9 files changed, 129 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 3d37f2c03..c8ffd7e44 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ location: # Paths of local or remote repositories to backup to. repositories: - - ssh://1234@usw-s001.rsync.net:backups.borg - - ssh://k8pDxu32@k8pDxu32.repo.borgbase.com:repo + - ssh://1234@usw-s001.rsync.net/backups.borg + - ssh://k8pDxu32@k8pDxu32.repo.borgbase.com/repo - /var/lib/backups/local.borg retention: diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index 60b71c8fd..4ddd27734 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -743,9 +743,10 @@ def load_configurations(config_filenames, overrides=None, resolve_env=True): # Parse and load each configuration file. for config_filename in config_filenames: try: - configs[config_filename] = validate.parse_configuration( + configs[config_filename], parse_logs = validate.parse_configuration( config_filename, validate.schema_filename(), overrides, resolve_env ) + logs.extend(parse_logs) except PermissionError: logs.extend( [ diff --git a/borgmatic/config/normalize.py b/borgmatic/config/normalize.py index cd7723a09..3e57f538c 100644 --- a/borgmatic/config/normalize.py +++ b/borgmatic/config/normalize.py @@ -1,8 +1,14 @@ -def normalize(config): +import logging + + +def normalize(config_filename, config): ''' - Given a configuration dict, apply particular hard-coded rules to normalize its contents to - adhere to the configuration schema. + Given a configuration filename and a configuration dict of its loaded contents, apply particular + hard-coded rules to normalize the configuration to adhere to the current schema. Return any log + message warnings produced based on the normalization performed. ''' + logs = [] + # Upgrade exclude_if_present from a string to a list. exclude_if_present = config.get('location', {}).get('exclude_if_present') if isinstance(exclude_if_present, str): @@ -29,3 +35,38 @@ def normalize(config): checks = config.get('consistency', {}).get('checks') if isinstance(checks, list) and len(checks) and isinstance(checks[0], str): config['consistency']['checks'] = [{'name': check_type} for check_type in checks] + + # Upgrade remote repositories to ssh:// syntax, required in Borg 2. + repositories = config.get('location', {}).get('repositories') + if repositories: + config['location']['repositories'] = [] + for repository in repositories: + # TODO: Instead of logging directly here, return logs and bubble them up to be displayed *after* logging is initialized. + if '~' in repository: + logs.append( + logging.makeLogRecord( + dict( + levelno=logging.WARNING, + levelname='WARNING', + msg=f'{config_filename}: Repository paths containing "~" are deprecated in borgmatic and no longer work in Borg 2.', + ) + ) + ) + if ':' in repository and not repository.startswith('ssh://'): + rewritten_repository = ( + f"ssh://{repository.replace(':~', '/~').replace(':/', '/').replace(':', '/./')}" + ) + logs.append( + logging.makeLogRecord( + dict( + levelno=logging.WARNING, + levelname='WARNING', + msg=f'{config_filename}: Remote repository paths without ssh:// syntax are deprecated. Interpreting "{repository}" as "{rewritten_repository}"', + ) + ) + ) + config['location']['repositories'].append(rewritten_repository) + else: + config['location']['repositories'].append(repository) + + return logs diff --git a/borgmatic/config/validate.py b/borgmatic/config/validate.py index b0e89cd9e..76f2a0f88 100644 --- a/borgmatic/config/validate.py +++ b/borgmatic/config/validate.py @@ -89,6 +89,9 @@ def parse_configuration(config_filename, schema_filename, overrides=None, resolv {'location': {'source_directories': ['/home', '/etc'], 'repository': 'hostname.borg'}, 'retention': {'keep_daily': 7}, 'consistency': {'checks': ['repository', 'archives']}} + Also return a sequence of logging.LogRecord instances containing any warnings about the + configuration. + 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. ''' @@ -99,7 +102,7 @@ def parse_configuration(config_filename, schema_filename, overrides=None, resolv raise Validation_error(config_filename, (str(error),)) override.apply_overrides(config, overrides) - normalize.normalize(config) + logs = normalize.normalize(config_filename, config) if resolve_env: environment.resolve_env_variables(config) @@ -116,7 +119,7 @@ def parse_configuration(config_filename, schema_filename, overrides=None, resolv apply_logical_validation(config_filename, config) - return config + return config, logs def normalize_repository_path(repository): diff --git a/docs/how-to/backup-to-a-removable-drive-or-an-intermittent-server.md b/docs/how-to/backup-to-a-removable-drive-or-an-intermittent-server.md index d3b2e02bb..f84fcf49e 100644 --- a/docs/how-to/backup-to-a-removable-drive-or-an-intermittent-server.md +++ b/docs/how-to/backup-to-a-removable-drive-or-an-intermittent-server.md @@ -76,7 +76,7 @@ location: - /home repositories: - - ssh://me@buddys-server.org:backup.borg + - ssh://me@buddys-server.org/backup.borg hooks: before_backup: diff --git a/docs/how-to/make-backups-redundant.md b/docs/how-to/make-backups-redundant.md index 7a5e5a36e..650270b8f 100644 --- a/docs/how-to/make-backups-redundant.md +++ b/docs/how-to/make-backups-redundant.md @@ -20,8 +20,8 @@ location: # Paths of local or remote repositories to backup to. repositories: - - ssh://1234@usw-s001.rsync.net:backups.borg - - ssh://k8pDxu32@k8pDxu32.repo.borgbase.com:repo + - ssh://1234@usw-s001.rsync.net/backups.borg + - ssh://k8pDxu32@k8pDxu32.repo.borgbase.com/repo - /var/lib/backups/local.borg ``` diff --git a/tests/integration/config/test_validate.py b/tests/integration/config/test_validate.py index 2cd164303..5d948ae2d 100644 --- a/tests/integration/config/test_validate.py +++ b/tests/integration/config/test_validate.py @@ -60,39 +60,39 @@ def test_parse_configuration_transforms_file_into_mapping(): ''' ) - result = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml') + config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml') - assert result == { + assert config == { 'location': {'source_directories': ['/home', '/etc'], 'repositories': ['hostname.borg']}, 'retention': {'keep_daily': 7, 'keep_hourly': 24, 'keep_minutely': 60}, 'consistency': {'checks': [{'name': 'repository'}, {'name': 'archives'}]}, } + assert logs == [] def test_parse_configuration_passes_through_quoted_punctuation(): escaped_punctuation = string.punctuation.replace('\\', r'\\').replace('"', r'\"') mock_config_and_schema( - ''' + f''' location: source_directories: - - /home + - "/home/{escaped_punctuation}" repositories: - - "{}.borg" - '''.format( - escaped_punctuation - ) + - test.borg + ''' ) - result = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml') + config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml') - assert result == { + assert config == { 'location': { - 'source_directories': ['/home'], - 'repositories': ['{}.borg'.format(string.punctuation)], + 'source_directories': [f'/home/{string.punctuation}'], + 'repositories': ['test.borg'], } } + assert logs == [] def test_parse_configuration_with_schema_lacking_examples_does_not_raise(): @@ -148,12 +148,13 @@ def test_parse_configuration_inlines_include(): include_file.name = 'include.yaml' builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file) - result = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml') + config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml') - assert result == { + assert config == { 'location': {'source_directories': ['/home'], 'repositories': ['hostname.borg']}, 'retention': {'keep_daily': 7, 'keep_hourly': 24}, } + assert logs == [] def test_parse_configuration_merges_include(): @@ -181,12 +182,13 @@ def test_parse_configuration_merges_include(): include_file.name = 'include.yaml' builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file) - result = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml') + config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml') - assert result == { + assert config == { 'location': {'source_directories': ['/home'], 'repositories': ['hostname.borg']}, 'retention': {'keep_daily': 1, 'keep_hourly': 24}, } + assert logs == [] def test_parse_configuration_raises_for_missing_config_file(): @@ -238,17 +240,18 @@ def test_parse_configuration_applies_overrides(): ''' ) - result = module.parse_configuration( + config, logs = module.parse_configuration( '/tmp/config.yaml', '/tmp/schema.yaml', overrides=['location.local_path=borg2'] ) - assert result == { + assert config == { 'location': { 'source_directories': ['/home'], 'repositories': ['hostname.borg'], 'local_path': 'borg2', } } + assert logs == [] def test_parse_configuration_applies_normalization(): @@ -265,12 +268,13 @@ def test_parse_configuration_applies_normalization(): ''' ) - result = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml') + config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml') - assert result == { + assert config == { 'location': { 'source_directories': ['/home'], 'repositories': ['hostname.borg'], 'exclude_if_present': ['.nobackup'], } } + assert logs == [] diff --git a/tests/unit/commands/test_borgmatic.py b/tests/unit/commands/test_borgmatic.py index 365400c50..2f67b469e 100644 --- a/tests/unit/commands/test_borgmatic.py +++ b/tests/unit/commands/test_borgmatic.py @@ -699,17 +699,19 @@ def test_run_actions_does_not_raise_for_borg_action(): ) -def test_load_configurations_collects_parsed_configurations(): +def test_load_configurations_collects_parsed_configurations_and_logs(): configuration = flexmock() other_configuration = flexmock() + test_expected_logs = [flexmock(), flexmock()] + other_expected_logs = [flexmock(), flexmock()] flexmock(module.validate).should_receive('parse_configuration').and_return( - configuration - ).and_return(other_configuration) + configuration, test_expected_logs + ).and_return(other_configuration, other_expected_logs) configs, logs = tuple(module.load_configurations(('test.yaml', 'other.yaml'))) assert configs == {'test.yaml': configuration, 'other.yaml': other_configuration} - assert logs == [] + assert logs == test_expected_logs + other_expected_logs def test_load_configurations_logs_warning_for_permission_error(): diff --git a/tests/unit/config/test_normalize.py b/tests/unit/config/test_normalize.py index 0927f9d2e..dfb41bb8f 100644 --- a/tests/unit/config/test_normalize.py +++ b/tests/unit/config/test_normalize.py @@ -4,44 +4,83 @@ from borgmatic.config import normalize as module @pytest.mark.parametrize( - 'config,expected_config', + 'config,expected_config,produces_logs', ( ( {'location': {'exclude_if_present': '.nobackup'}}, {'location': {'exclude_if_present': ['.nobackup']}}, + False, ), ( {'location': {'exclude_if_present': ['.nobackup']}}, {'location': {'exclude_if_present': ['.nobackup']}}, + False, ), ( {'location': {'source_directories': ['foo', 'bar']}}, {'location': {'source_directories': ['foo', 'bar']}}, + False, + ), + ( + {'storage': {'compression': 'yes_please'}}, + {'storage': {'compression': 'yes_please'}}, + False, ), - ({'storage': {'compression': 'yes_please'}}, {'storage': {'compression': 'yes_please'}}), ( {'hooks': {'healthchecks': 'https://example.com'}}, {'hooks': {'healthchecks': {'ping_url': 'https://example.com'}}}, + False, ), ( {'hooks': {'cronitor': 'https://example.com'}}, {'hooks': {'cronitor': {'ping_url': 'https://example.com'}}}, + False, ), ( {'hooks': {'pagerduty': 'https://example.com'}}, {'hooks': {'pagerduty': {'integration_key': 'https://example.com'}}}, + False, ), ( {'hooks': {'cronhub': 'https://example.com'}}, {'hooks': {'cronhub': {'ping_url': 'https://example.com'}}}, + False, ), ( {'consistency': {'checks': ['archives']}}, {'consistency': {'checks': [{'name': 'archives'}]}}, + False, + ), + ( + {'location': {'repositories': ['foo@bar:/repo']}}, + {'location': {'repositories': ['ssh://foo@bar/repo']}}, + True, + ), + ( + {'location': {'repositories': ['foo@bar:repo']}}, + {'location': {'repositories': ['ssh://foo@bar/./repo']}}, + True, + ), + ( + {'location': {'repositories': ['foo@bar:~/repo']}}, + {'location': {'repositories': ['ssh://foo@bar/~/repo']}}, + True, + ), + ( + {'location': {'repositories': ['ssh://foo@bar/repo']}}, + {'location': {'repositories': ['ssh://foo@bar/repo']}}, + False, ), ), ) -def test_normalize_applies_hard_coded_normalization_to_config(config, expected_config): - module.normalize(config) +def test_normalize_applies_hard_coded_normalization_to_config( + config, expected_config, produces_logs +): + logs = module.normalize('test.yaml', config) assert config == expected_config + + if produces_logs: + assert logs + else: + assert logs == [] From 596dd49cf580185dae534bebb59d71a002308e18 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Wed, 17 Aug 2022 14:26:35 -0700 Subject: [PATCH 24/38] Use --glob-archives instead of --prefix on rlist command (#557). --- borgmatic/borg/list.py | 2 +- borgmatic/borg/rlist.py | 9 ++++++-- tests/unit/borg/test_rlist.py | 41 +++++++++++++++++++++++++---------- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/borgmatic/borg/list.py b/borgmatic/borg/list.py index cbca8d7c7..3c2addb24 100644 --- a/borgmatic/borg/list.py +++ b/borgmatic/borg/list.py @@ -41,7 +41,7 @@ def make_list_command( ) + flags.make_flags('remote-path', remote_path) + flags.make_flags('lock-wait', lock_wait) - + flags.make_flags_from_arguments(list_arguments, excludes=MAKE_FLAGS_EXCLUDES,) + + flags.make_flags_from_arguments(list_arguments, excludes=MAKE_FLAGS_EXCLUDES) + ( flags.make_repository_archive_flags( repository, list_arguments.archive, local_borg_version diff --git a/borgmatic/borg/rlist.py b/borgmatic/borg/rlist.py index a4cd1ef3e..c2e9299db 100644 --- a/borgmatic/borg/rlist.py +++ b/borgmatic/borg/rlist.py @@ -51,7 +51,7 @@ def resolve_archive_name( return latest_archive -MAKE_FLAGS_EXCLUDES = ('repository',) +MAKE_FLAGS_EXCLUDES = ('repository', 'prefix') def make_rlist_command( @@ -86,7 +86,12 @@ def make_rlist_command( ) + flags.make_flags('remote-path', remote_path) + flags.make_flags('lock-wait', lock_wait) - + flags.make_flags_from_arguments(rlist_arguments, excludes=MAKE_FLAGS_EXCLUDES,) + + ( + flags.make_flags('glob-archives', f'{rlist_arguments.prefix}*') + if rlist_arguments.prefix + else () + ) + + flags.make_flags_from_arguments(rlist_arguments, excludes=MAKE_FLAGS_EXCLUDES) + flags.make_repository_flags(repository, local_borg_version) ) diff --git a/tests/unit/borg/test_rlist.py b/tests/unit/borg/test_rlist.py index 816843551..d16b53db7 100644 --- a/tests/unit/borg/test_rlist.py +++ b/tests/unit/borg/test_rlist.py @@ -153,7 +153,7 @@ def test_make_rlist_command_includes_log_info(): repository='repo', storage_config={}, local_borg_version='1.2.3', - rlist_arguments=flexmock(archive=None, paths=None, json=False), + rlist_arguments=flexmock(archive=None, paths=None, json=False, prefix=None), ) assert command == ('borg', 'list', '--info', 'repo') @@ -169,7 +169,7 @@ def test_make_rlist_command_includes_json_but_not_info(): repository='repo', storage_config={}, local_borg_version='1.2.3', - rlist_arguments=flexmock(archive=None, paths=None, json=True), + rlist_arguments=flexmock(archive=None, paths=None, json=True, prefix=None), ) assert command == ('borg', 'list', '--json', 'repo') @@ -185,7 +185,7 @@ def test_make_rlist_command_includes_log_debug(): repository='repo', storage_config={}, local_borg_version='1.2.3', - rlist_arguments=flexmock(archive=None, paths=None, json=False), + rlist_arguments=flexmock(archive=None, paths=None, json=False, prefix=None), ) assert command == ('borg', 'list', '--debug', '--show-rc', 'repo') @@ -201,7 +201,7 @@ def test_make_rlist_command_includes_json_but_not_debug(): repository='repo', storage_config={}, local_borg_version='1.2.3', - rlist_arguments=flexmock(archive=None, paths=None, json=True), + rlist_arguments=flexmock(archive=None, paths=None, json=True, prefix=None), ) assert command == ('borg', 'list', '--json', 'repo') @@ -216,7 +216,7 @@ def test_make_rlist_command_includes_json(): repository='repo', storage_config={}, local_borg_version='1.2.3', - rlist_arguments=flexmock(archive=None, paths=None, json=True), + rlist_arguments=flexmock(archive=None, paths=None, json=True, prefix=None), ) assert command == ('borg', 'list', '--json', 'repo') @@ -225,7 +225,7 @@ def test_make_rlist_command_includes_json(): def test_make_rlist_command_includes_lock_wait(): flexmock(module.flags).should_receive('make_flags').and_return(()).and_return( ('--lock-wait', '5') - ) + ).and_return(()) flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) @@ -233,7 +233,7 @@ def test_make_rlist_command_includes_lock_wait(): repository='repo', storage_config={'lock_wait': 5}, local_borg_version='1.2.3', - rlist_arguments=flexmock(archive=None, paths=None, json=False), + rlist_arguments=flexmock(archive=None, paths=None, json=False, prefix=None), ) assert command == ('borg', 'list', '--lock-wait', '5', 'repo') @@ -248,7 +248,7 @@ def test_make_rlist_command_includes_local_path(): repository='repo', storage_config={}, local_borg_version='1.2.3', - rlist_arguments=flexmock(archive=None, paths=None, json=False), + rlist_arguments=flexmock(archive=None, paths=None, json=False, prefix=None), local_path='borg2', ) @@ -258,7 +258,7 @@ def test_make_rlist_command_includes_local_path(): def test_make_rlist_command_includes_remote_path(): flexmock(module.flags).should_receive('make_flags').and_return( ('--remote-path', 'borg2') - ).and_return(()) + ).and_return(()).and_return(()) flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) @@ -266,13 +266,30 @@ def test_make_rlist_command_includes_remote_path(): repository='repo', storage_config={}, local_borg_version='1.2.3', - rlist_arguments=flexmock(archive=None, paths=None, json=False), + rlist_arguments=flexmock(archive=None, paths=None, json=False, prefix=None), remote_path='borg2', ) assert command == ('borg', 'list', '--remote-path', 'borg2', 'repo') +def test_make_rlist_command_transforms_prefix_into_glob_archives(): + flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(()).and_return( + ('--glob-archives', 'foo*') + ) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + + command = module.make_rlist_command( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=flexmock(archive=None, paths=None, json=False, prefix='foo'), + ) + + assert command == ('borg', 'list', '--glob-archives', 'foo*', 'repo') + + def test_make_rlist_command_includes_short(): flexmock(module.flags).should_receive('make_flags').and_return(()) flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--short',)) @@ -282,7 +299,7 @@ def test_make_rlist_command_includes_short(): repository='repo', storage_config={}, local_borg_version='1.2.3', - rlist_arguments=flexmock(archive=None, paths=None, json=False, short=True), + rlist_arguments=flexmock(archive=None, paths=None, json=False, prefix=None, short=True), ) assert command == ('borg', 'list', '--short', 'repo') @@ -291,7 +308,6 @@ def test_make_rlist_command_includes_short(): @pytest.mark.parametrize( 'argument_name', ( - 'prefix', 'glob_archives', 'sort_by', 'first', @@ -317,6 +333,7 @@ def test_make_rlist_command_includes_additional_flags(argument_name): archive=None, paths=None, json=False, + prefix=None, find_paths=None, format=None, **{argument_name: 'value'}, From 3ab7a3b64ac7a51841d7e5ba42fabd6cc440b4ab Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Wed, 17 Aug 2022 15:36:19 -0700 Subject: [PATCH 25/38] Replace use of --prefix with --glob-archives in info action (#557). --- borgmatic/borg/info.py | 20 ++- borgmatic/commands/arguments.py | 14 +- tests/integration/commands/test_arguments.py | 14 ++ tests/unit/borg/test_info.py | 129 ++++++++++++++++--- 4 files changed, 149 insertions(+), 28 deletions(-) diff --git a/borgmatic/borg/info.py b/borgmatic/borg/info.py index 9e56f0ede..64ee89a9a 100644 --- a/borgmatic/borg/info.py +++ b/borgmatic/borg/info.py @@ -1,7 +1,6 @@ import logging -from borgmatic.borg import environment, feature -from borgmatic.borg.flags import make_flags, make_flags_from_arguments +from borgmatic.borg import environment, feature, flags from borgmatic.execute import execute_command logger = logging.getLogger(__name__) @@ -34,13 +33,20 @@ def display_archives_info( if logger.isEnabledFor(logging.DEBUG) and not info_arguments.json else () ) - + make_flags('remote-path', remote_path) - + make_flags('lock-wait', lock_wait) - + make_flags_from_arguments(info_arguments, excludes=('repository', 'archive')) + + flags.make_flags('remote-path', remote_path) + + flags.make_flags('lock-wait', lock_wait) + + ( + flags.make_flags('glob-archives', f'{info_arguments.prefix}*') + if info_arguments.prefix + else () + ) + + flags.make_flags_from_arguments( + info_arguments, excludes=('repository', 'archive', 'prefix') + ) + ( ( - ('--repo', repository) - + (('--glob-archives', info_arguments.archive) if info_arguments.archive else ()) + flags.make_flags('repo', repository) + + flags.make_flags('glob-archives', info_arguments.archive) ) if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version) else ( diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index 8a2ed346f..d817f26e1 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -745,22 +745,26 @@ def parse_arguments(*unparsed_arguments): if arguments['global'].excludes_filename: raise ValueError( - 'The --excludes flag has been replaced with exclude_patterns in configuration' + 'The --excludes flag has been replaced with exclude_patterns in configuration.' ) if 'rcreate' in arguments and arguments['global'].dry_run: - raise ValueError('The rcreate/init action cannot be used with the --dry-run flag') + raise ValueError('The rcreate/init action cannot be used with the --dry-run flag.') if ( ('list' in arguments and 'rinfo' in arguments and arguments['list'].json) or ('list' in arguments and 'info' in arguments and arguments['list'].json) or ('rinfo' in arguments and 'info' in arguments and arguments['rinfo'].json) ): - raise ValueError('With the --json flag, multiple actions cannot be used together') + raise ValueError('With the --json flag, multiple actions cannot be used together.') - if 'info' in arguments and arguments['info'].archive and arguments['info'].glob_archives: + if 'info' in arguments and ( + (arguments['info'].archive and arguments['info'].prefix) + or (arguments['info'].archive and arguments['info'].glob_archives) + or (arguments['info'].prefix and arguments['info'].glob_archives) + ): raise ValueError( - 'With the info action, the --archive and --glob-archives flags cannot be used together' + 'With the info action, only one of --archive, --prefix, or --glob-archives flags can be used.' ) return arguments diff --git a/tests/integration/commands/test_arguments.py b/tests/integration/commands/test_arguments.py index a9e4f5cc8..b70135630 100644 --- a/tests/integration/commands/test_arguments.py +++ b/tests/integration/commands/test_arguments.py @@ -517,6 +517,20 @@ def test_parse_arguments_disallows_info_with_both_archive_and_glob_archives(): module.parse_arguments('info', '--archive', 'foo', '--glob-archives', '*bar') +def test_parse_arguments_disallows_info_with_both_archive_and_prefix(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(ValueError): + module.parse_arguments('info', '--archive', 'foo', '--prefix', 'bar') + + +def test_parse_arguments_disallows_info_with_both_prefix_and_glob_archives(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(ValueError): + module.parse_arguments('info', '--prefix', 'foo', '--glob-archives', '*bar') + + def test_parse_arguments_check_only_extract_does_not_raise_extract_subparser_error(): flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) diff --git a/tests/unit/borg/test_info.py b/tests/unit/borg/test_info.py index 8817bb7ee..0121d61d1 100644 --- a/tests/unit/borg/test_info.py +++ b/tests/unit/borg/test_info.py @@ -9,6 +9,11 @@ from ..test_verbosity import insert_logging_mock def test_display_archives_info_calls_borg_with_parameters(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args('repo', 'repo').and_return( + ('--repo', 'repo') + ) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( @@ -22,11 +27,13 @@ def test_display_archives_info_calls_borg_with_parameters(): repository='repo', storage_config={}, local_borg_version='2.3.4', - info_arguments=flexmock(archive=None, json=False), + info_arguments=flexmock(archive=None, json=False, prefix=None), ) def test_display_archives_info_without_borg_features_calls_borg_without_repo_flag(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) flexmock(module.feature).should_receive('available').and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( @@ -40,11 +47,16 @@ def test_display_archives_info_without_borg_features_calls_borg_without_repo_fla repository='repo', storage_config={}, local_borg_version='2.3.4', - info_arguments=flexmock(archive=None, json=False), + info_arguments=flexmock(archive=None, json=False, prefix=None), ) def test_display_archives_info_with_log_info_calls_borg_with_info_parameter(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args('repo', 'repo').and_return( + ('--repo', 'repo') + ) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( @@ -58,11 +70,16 @@ def test_display_archives_info_with_log_info_calls_borg_with_info_parameter(): repository='repo', storage_config={}, local_borg_version='2.3.4', - info_arguments=flexmock(archive=None, json=False), + info_arguments=flexmock(archive=None, json=False, prefix=None), ) def test_display_archives_info_with_log_info_and_json_suppresses_most_borg_output(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args('repo', 'repo').and_return( + ('--repo', 'repo') + ) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',)) flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( @@ -77,13 +94,18 @@ def test_display_archives_info_with_log_info_and_json_suppresses_most_borg_outpu repository='repo', storage_config={}, local_borg_version='2.3.4', - info_arguments=flexmock(archive=None, json=True), + info_arguments=flexmock(archive=None, json=True, prefix=None), ) assert json_output == '[]' def test_display_archives_info_with_log_debug_calls_borg_with_debug_parameter(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args('repo', 'repo').and_return( + ('--repo', 'repo') + ) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( @@ -98,11 +120,16 @@ def test_display_archives_info_with_log_debug_calls_borg_with_debug_parameter(): repository='repo', storage_config={}, local_borg_version='2.3.4', - info_arguments=flexmock(archive=None, json=False), + info_arguments=flexmock(archive=None, json=False, prefix=None), ) def test_display_archives_info_with_log_debug_and_json_suppresses_most_borg_output(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args('repo', 'repo').and_return( + ('--repo', 'repo') + ) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',)) flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( @@ -117,13 +144,18 @@ def test_display_archives_info_with_log_debug_and_json_suppresses_most_borg_outp repository='repo', storage_config={}, local_borg_version='2.3.4', - info_arguments=flexmock(archive=None, json=True), + info_arguments=flexmock(archive=None, json=True, prefix=None), ) assert json_output == '[]' def test_display_archives_info_with_json_calls_borg_with_json_parameter(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args('repo', 'repo').and_return( + ('--repo', 'repo') + ) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',)) flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( @@ -137,13 +169,21 @@ def test_display_archives_info_with_json_calls_borg_with_json_parameter(): repository='repo', storage_config={}, local_borg_version='2.3.4', - info_arguments=flexmock(archive=None, json=True), + info_arguments=flexmock(archive=None, json=True, prefix=None), ) assert json_output == '[]' def test_display_archives_info_with_archive_calls_borg_with_glob_archives_parameter(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args('repo', 'repo').and_return( + ('--repo', 'repo') + ) + flexmock(module.flags).should_receive('make_flags').with_args( + 'glob-archives', 'archive' + ).and_return(('--glob-archives', 'archive')) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( @@ -157,11 +197,13 @@ def test_display_archives_info_with_archive_calls_borg_with_glob_archives_parame repository='repo', storage_config={}, local_borg_version='2.3.4', - info_arguments=flexmock(archive='archive', json=False), + info_arguments=flexmock(archive='archive', json=False, prefix=None), ) def test_display_archives_info_with_archive_and_without_borg_features_calls_borg_with_repo_archive_parameter(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) flexmock(module.feature).should_receive('available').and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( @@ -175,11 +217,16 @@ def test_display_archives_info_with_archive_and_without_borg_features_calls_borg repository='repo', storage_config={}, local_borg_version='2.3.4', - info_arguments=flexmock(archive='archive', json=False), + info_arguments=flexmock(archive='archive', json=False, prefix=None), ) def test_display_archives_info_with_local_path_calls_borg_via_local_path(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args('repo', 'repo').and_return( + ('--repo', 'repo') + ) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( @@ -193,12 +240,20 @@ def test_display_archives_info_with_local_path_calls_borg_via_local_path(): repository='repo', storage_config={}, local_borg_version='2.3.4', - info_arguments=flexmock(archive=None, json=False), + info_arguments=flexmock(archive=None, json=False, prefix=None), local_path='borg1', ) def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_parameters(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args('repo', 'repo').and_return( + ('--repo', 'repo') + ) + flexmock(module.flags).should_receive('make_flags').with_args( + 'remote-path', 'borg1' + ).and_return(('--remote-path', 'borg1')) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( @@ -212,12 +267,20 @@ def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_para repository='repo', storage_config={}, local_borg_version='2.3.4', - info_arguments=flexmock(archive=None, json=False), + info_arguments=flexmock(archive=None, json=False, prefix=None), remote_path='borg1', ) def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_parameters(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args('repo', 'repo').and_return( + ('--repo', 'repo') + ) + flexmock(module.flags).should_receive('make_flags').with_args('lock-wait', 5).and_return( + ('--lock-wait', '5') + ) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) storage_config = {'lock_wait': 5} flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') @@ -232,16 +295,23 @@ def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_paramete repository='repo', storage_config=storage_config, local_borg_version='2.3.4', - info_arguments=flexmock(archive=None, json=False), + info_arguments=flexmock(archive=None, json=False, prefix=None), ) -@pytest.mark.parametrize('argument_name', ('prefix', 'glob_archives', 'sort_by', 'first', 'last')) -def test_display_archives_info_passes_through_arguments_to_borg(argument_name): +def test_display_archives_info_with_prefix_calls_borg_with_glob_archives_parameters(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args('repo', 'repo').and_return( + ('--repo', 'repo') + ) + flexmock(module.flags).should_receive('make_flags').with_args( + 'glob-archives', 'foo*' + ).and_return(('--glob-archives', 'foo*')) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'info', '--' + argument_name.replace('_', '-'), 'value', '--repo', 'repo'), + ('borg', 'info', '--glob-archives', 'foo*', '--repo', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg', extra_environment=None, @@ -251,5 +321,32 @@ def test_display_archives_info_passes_through_arguments_to_borg(argument_name): repository='repo', storage_config={}, local_borg_version='2.3.4', - info_arguments=flexmock(archive=None, json=False, **{argument_name: 'value'}), + info_arguments=flexmock(archive=None, json=False, prefix='foo'), + ) + + +@pytest.mark.parametrize('argument_name', ('glob_archives', 'sort_by', 'first', 'last')) +def test_display_archives_info_passes_through_arguments_to_borg(argument_name): + flag_name = f"--{argument_name.replace('_', ' ')}" + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args('repo', 'repo').and_return( + ('--repo', 'repo') + ) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return( + (flag_name, 'value') + ) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'info', flag_name, 'value', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.display_archives_info( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + info_arguments=flexmock(archive=None, json=False, prefix=None, **{argument_name: 'value'}), ) From 57009e22b50883167c807015a91f2a8061bbc65a Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Wed, 17 Aug 2022 17:11:02 -0700 Subject: [PATCH 26/38] Use flag-related utility functions in info action (#557). --- borgmatic/borg/info.py | 16 +++++------- tests/unit/borg/test_info.py | 50 ++++++++++-------------------------- 2 files changed, 21 insertions(+), 45 deletions(-) diff --git a/borgmatic/borg/info.py b/borgmatic/borg/info.py index 64ee89a9a..9e7f484e6 100644 --- a/borgmatic/borg/info.py +++ b/borgmatic/borg/info.py @@ -44,15 +44,13 @@ def display_archives_info( info_arguments, excludes=('repository', 'archive', 'prefix') ) + ( - ( - flags.make_flags('repo', repository) - + flags.make_flags('glob-archives', info_arguments.archive) - ) - if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version) - else ( - '::'.join((repository, info_arguments.archive)) - if info_arguments.archive - else repository, + flags.make_repository_flags(repository, local_borg_version) + + ( + flags.make_flags('glob-archives', info_arguments.archive) + if feature.available( + feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version + ) + else () ) ) ) diff --git a/tests/unit/borg/test_info.py b/tests/unit/borg/test_info.py index 0121d61d1..a6689ac5a 100644 --- a/tests/unit/borg/test_info.py +++ b/tests/unit/borg/test_info.py @@ -10,10 +10,8 @@ from ..test_verbosity import insert_logging_mock def test_display_archives_info_calls_borg_with_parameters(): flexmock(module.flags).should_receive('make_flags').and_return(()) - flexmock(module.flags).should_receive('make_flags').with_args('repo', 'repo').and_return( - ('--repo', 'repo') - ) flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( @@ -34,6 +32,7 @@ def test_display_archives_info_calls_borg_with_parameters(): def test_display_archives_info_without_borg_features_calls_borg_without_repo_flag(): flexmock(module.flags).should_receive('make_flags').and_return(()) flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) flexmock(module.feature).should_receive('available').and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( @@ -53,10 +52,8 @@ def test_display_archives_info_without_borg_features_calls_borg_without_repo_fla def test_display_archives_info_with_log_info_calls_borg_with_info_parameter(): flexmock(module.flags).should_receive('make_flags').and_return(()) - flexmock(module.flags).should_receive('make_flags').with_args('repo', 'repo').and_return( - ('--repo', 'repo') - ) flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( @@ -76,10 +73,8 @@ def test_display_archives_info_with_log_info_calls_borg_with_info_parameter(): def test_display_archives_info_with_log_info_and_json_suppresses_most_borg_output(): flexmock(module.flags).should_receive('make_flags').and_return(()) - flexmock(module.flags).should_receive('make_flags').with_args('repo', 'repo').and_return( - ('--repo', 'repo') - ) flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',)) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( @@ -102,10 +97,8 @@ def test_display_archives_info_with_log_info_and_json_suppresses_most_borg_outpu def test_display_archives_info_with_log_debug_calls_borg_with_debug_parameter(): flexmock(module.flags).should_receive('make_flags').and_return(()) - flexmock(module.flags).should_receive('make_flags').with_args('repo', 'repo').and_return( - ('--repo', 'repo') - ) flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( @@ -126,10 +119,8 @@ def test_display_archives_info_with_log_debug_calls_borg_with_debug_parameter(): def test_display_archives_info_with_log_debug_and_json_suppresses_most_borg_output(): flexmock(module.flags).should_receive('make_flags').and_return(()) - flexmock(module.flags).should_receive('make_flags').with_args('repo', 'repo').and_return( - ('--repo', 'repo') - ) flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',)) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( @@ -152,10 +143,8 @@ def test_display_archives_info_with_log_debug_and_json_suppresses_most_borg_outp def test_display_archives_info_with_json_calls_borg_with_json_parameter(): flexmock(module.flags).should_receive('make_flags').and_return(()) - flexmock(module.flags).should_receive('make_flags').with_args('repo', 'repo').and_return( - ('--repo', 'repo') - ) flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',)) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( @@ -177,13 +166,11 @@ def test_display_archives_info_with_json_calls_borg_with_json_parameter(): def test_display_archives_info_with_archive_calls_borg_with_glob_archives_parameter(): flexmock(module.flags).should_receive('make_flags').and_return(()) - flexmock(module.flags).should_receive('make_flags').with_args('repo', 'repo').and_return( - ('--repo', 'repo') - ) flexmock(module.flags).should_receive('make_flags').with_args( 'glob-archives', 'archive' ).and_return(('--glob-archives', 'archive')) flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( @@ -204,6 +191,7 @@ def test_display_archives_info_with_archive_calls_borg_with_glob_archives_parame def test_display_archives_info_with_archive_and_without_borg_features_calls_borg_with_repo_archive_parameter(): flexmock(module.flags).should_receive('make_flags').and_return(()) flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo::archive',)) flexmock(module.feature).should_receive('available').and_return(False) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( @@ -223,11 +211,9 @@ def test_display_archives_info_with_archive_and_without_borg_features_calls_borg def test_display_archives_info_with_local_path_calls_borg_via_local_path(): flexmock(module.flags).should_receive('make_flags').and_return(()) - flexmock(module.flags).should_receive('make_flags').with_args('repo', 'repo').and_return( - ('--repo', 'repo') - ) flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg1', 'info', '--repo', 'repo'), @@ -247,13 +233,11 @@ def test_display_archives_info_with_local_path_calls_borg_via_local_path(): def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_parameters(): flexmock(module.flags).should_receive('make_flags').and_return(()) - flexmock(module.flags).should_receive('make_flags').with_args('repo', 'repo').and_return( - ('--repo', 'repo') - ) flexmock(module.flags).should_receive('make_flags').with_args( 'remote-path', 'borg1' ).and_return(('--remote-path', 'borg1')) flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( @@ -274,13 +258,11 @@ def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_para def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_parameters(): flexmock(module.flags).should_receive('make_flags').and_return(()) - flexmock(module.flags).should_receive('make_flags').with_args('repo', 'repo').and_return( - ('--repo', 'repo') - ) flexmock(module.flags).should_receive('make_flags').with_args('lock-wait', 5).and_return( ('--lock-wait', '5') ) flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) storage_config = {'lock_wait': 5} flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') @@ -301,13 +283,11 @@ def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_paramete def test_display_archives_info_with_prefix_calls_borg_with_glob_archives_parameters(): flexmock(module.flags).should_receive('make_flags').and_return(()) - flexmock(module.flags).should_receive('make_flags').with_args('repo', 'repo').and_return( - ('--repo', 'repo') - ) flexmock(module.flags).should_receive('make_flags').with_args( 'glob-archives', 'foo*' ).and_return(('--glob-archives', 'foo*')) flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( @@ -329,12 +309,10 @@ def test_display_archives_info_with_prefix_calls_borg_with_glob_archives_paramet def test_display_archives_info_passes_through_arguments_to_borg(argument_name): flag_name = f"--{argument_name.replace('_', ' ')}" flexmock(module.flags).should_receive('make_flags').and_return(()) - flexmock(module.flags).should_receive('make_flags').with_args('repo', 'repo').and_return( - ('--repo', 'repo') - ) flexmock(module.flags).should_receive('make_flags_from_arguments').and_return( (flag_name, 'value') ) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( From 3b6ed0668648bd8da20babd1eb9139b91e3b4430 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Wed, 17 Aug 2022 17:33:09 -0700 Subject: [PATCH 27/38] Add --other-repo flag to rcreate action (#557). --- borgmatic/borg/rcreate.py | 7 +++++-- borgmatic/commands/arguments.py | 5 +++++ borgmatic/commands/borgmatic.py | 1 + tests/unit/borg/test_rcreate.py | 15 +++++++++++++++ tests/unit/commands/test_borgmatic.py | 5 ++++- 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/borgmatic/borg/rcreate.py b/borgmatic/borg/rcreate.py index 5c0be8961..6babe0170 100644 --- a/borgmatic/borg/rcreate.py +++ b/borgmatic/borg/rcreate.py @@ -16,6 +16,7 @@ def create_repository( storage_config, local_borg_version, encryption_mode, + other_repo=None, append_only=None, storage_quota=None, local_path='borg', @@ -23,8 +24,9 @@ def create_repository( ): ''' Given a local or remote repository path, a storage configuration dict, the local Borg version, a - Borg encryption mode, whether the repository should be append-only, and the storage quota to - use, create the repository. If the repository already exists, then log and skip creation. + Borg encryption mode, the path to another repo whose key material should be reused, whether the + repository should be append-only, and the storage quota to use, create the repository. If the + repository already exists, then log and skip creation. ''' try: rinfo.display_repository_info( @@ -51,6 +53,7 @@ def create_repository( else ('init',) ) + (('--encryption', encryption_mode) if encryption_mode else ()) + + (('--other-repo', other_repo) if other_repo else ()) + (('--append-only',) if append_only else ()) + (('--storage-quota', storage_quota) if storage_quota else ()) + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ()) diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index d817f26e1..bcc4b7450 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -239,6 +239,11 @@ def make_parsers(): help='Borg repository encryption mode', required=True, ) + rcreate_group.add_argument( + '--other-repo', + metavar='SOURCE_REPOSITORY', + help='Path to an existing Borg repository whose key material should be reused (Borg 2.x+ only)', + ) rcreate_group.add_argument( '--append-only', dest='append_only', diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index 4ddd27734..343b9a64d 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -258,6 +258,7 @@ def run_actions( storage, local_borg_version, arguments['rcreate'].encryption_mode, + arguments['rcreate'].other_repo, arguments['rcreate'].append_only, arguments['rcreate'].storage_quota, local_path=local_path, diff --git a/tests/unit/borg/test_rcreate.py b/tests/unit/borg/test_rcreate.py index 292534588..1a22e8e01 100644 --- a/tests/unit/borg/test_rcreate.py +++ b/tests/unit/borg/test_rcreate.py @@ -85,6 +85,21 @@ def test_create_repository_raises_for_unknown_rinfo_command_error(): ) +def test_create_repository_with_append_only_calls_borg_with_other_repo_parameter(): + insert_rinfo_command_not_found_mock() + insert_rcreate_command_mock(RCREATE_COMMAND + ('--other-repo', 'other.borg', '--repo', 'repo')) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + + module.create_repository( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', + other_repo='other.borg', + ) + + def test_create_repository_with_append_only_calls_borg_with_append_only_parameter(): insert_rinfo_command_not_found_mock() insert_rcreate_command_mock(RCREATE_COMMAND + ('--append-only', '--repo', 'repo')) diff --git a/tests/unit/commands/test_borgmatic.py b/tests/unit/commands/test_borgmatic.py index 2f67b469e..670377e7f 100644 --- a/tests/unit/commands/test_borgmatic.py +++ b/tests/unit/commands/test_borgmatic.py @@ -345,7 +345,10 @@ def test_run_actions_does_not_raise_for_rcreate_action(): arguments = { 'global': flexmock(monitoring_verbosity=1, dry_run=False), 'rcreate': flexmock( - encryption_mode=flexmock(), append_only=flexmock(), storage_quota=flexmock() + encryption_mode=flexmock(), + other_repo=flexmock(), + append_only=flexmock(), + storage_quota=flexmock(), ), } From f47c98c4a51eb0ae970819297a57080ae8936293 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Wed, 17 Aug 2022 21:14:58 -0700 Subject: [PATCH 28/38] Rename several configuration options to match Borg 2 (#557). --- NEWS | 9 +++++++++ borgmatic/borg/create.py | 14 +++++++------- borgmatic/borg/extract.py | 4 ++-- borgmatic/commands/arguments.py | 4 ++-- borgmatic/config/generate.py | 2 +- borgmatic/config/normalize.py | 14 +++++++++++++- borgmatic/config/schema.yaml | 8 ++++---- tests/unit/borg/test_create.py | 12 ++++++------ tests/unit/borg/test_extract.py | 2 +- tests/unit/config/test_normalize.py | 7 +++++++ 10 files changed, 52 insertions(+), 24 deletions(-) diff --git a/NEWS b/NEWS index b753753b2..b20033c87 100644 --- a/NEWS +++ b/NEWS @@ -4,6 +4,15 @@ repository info). If you install Borg 2, you'll need to manually "borg transfer" or "borgmatic transfer" your existing Borg 1 repositories before use. See the Borg 2.0 changelog for more information about Borg 2: https://www.borgbackup.org/releases/borg-2.0.html + * #557: Rename several configuration options to match Borg 2: "remote_rate_limit" is now + "upload_rate_limit", "numeric_owner" is "numeric_ids", and "bsd_flags" is "flags". borgmatic + still works with the old options. + * #557: Remote repository paths without the "ssh://" syntax are deprecated but still supported for + now. However, remote repository paths containing "~" will no longer work. + * #557: Omitting the "--archive" flag on the "list" action is deprecated when using Borg 2. Use + the new "rlist" action instead. And when using the "--archive" or "--find" flags on the "list" + action with Borg 2, several flags are no longer supported: "--prefix", "--glob-archives", + "--sort-by", "--first", and "--last". * #565: Fix handling of "repository" and "data" consistency checks to prevent invalid Borg flags. * #566: Modify "mount" and "extract" actions to require the "--repository" flag when multiple repositories are configured. diff --git a/borgmatic/borg/create.py b/borgmatic/borg/create.py index 6cee09cba..9977e05a3 100644 --- a/borgmatic/borg/create.py +++ b/borgmatic/borg/create.py @@ -233,7 +233,7 @@ def create_archive( checkpoint_interval = storage_config.get('checkpoint_interval', None) chunker_params = storage_config.get('chunker_params', None) compression = storage_config.get('compression', None) - remote_rate_limit = storage_config.get('remote_rate_limit', None) + upload_rate_limit = storage_config.get('upload_rate_limit', None) umask = storage_config.get('umask', None) lock_wait = storage_config.get('lock_wait', None) files_cache = location_config.get('files_cache') @@ -246,22 +246,22 @@ def create_archive( atime_flags = ('--noatime',) if location_config.get('atime') is False else () if feature.available(feature.Feature.NOFLAGS, local_borg_version): - noflags_flags = ('--noflags',) if location_config.get('bsd_flags') is False else () + noflags_flags = ('--noflags',) if location_config.get('flags') is False else () else: - noflags_flags = ('--nobsdflags',) if location_config.get('bsd_flags') is False else () + noflags_flags = ('--nobsdflags',) if location_config.get('flags') is False else () if feature.available(feature.Feature.NUMERIC_IDS, local_borg_version): - numeric_ids_flags = ('--numeric-ids',) if location_config.get('numeric_owner') else () + numeric_ids_flags = ('--numeric-ids',) if location_config.get('numeric_ids') else () else: - numeric_ids_flags = ('--numeric-owner',) if location_config.get('numeric_owner') else () + numeric_ids_flags = ('--numeric-owner',) if location_config.get('numeric_ids') else () if feature.available(feature.Feature.UPLOAD_RATELIMIT, local_borg_version): upload_ratelimit_flags = ( - ('--upload-ratelimit', str(remote_rate_limit)) if remote_rate_limit else () + ('--upload-ratelimit', str(upload_rate_limit)) if upload_rate_limit else () ) else: upload_ratelimit_flags = ( - ('--remote-ratelimit', str(remote_rate_limit)) if remote_rate_limit else () + ('--remote-ratelimit', str(upload_rate_limit)) if upload_rate_limit else () ) ensure_files_readable(location_config.get('patterns_from'), location_config.get('exclude_from')) diff --git a/borgmatic/borg/extract.py b/borgmatic/borg/extract.py index 5e2a58d2c..8ea8bbbd3 100644 --- a/borgmatic/borg/extract.py +++ b/borgmatic/borg/extract.py @@ -83,9 +83,9 @@ def extract_archive( raise ValueError('progress and extract_to_stdout cannot both be set') if feature.available(feature.Feature.NUMERIC_IDS, local_borg_version): - numeric_ids_flags = ('--numeric-ids',) if location_config.get('numeric_owner') else () + numeric_ids_flags = ('--numeric-ids',) if location_config.get('numeric_ids') else () else: - numeric_ids_flags = ('--numeric-owner',) if location_config.get('numeric_owner') else () + numeric_ids_flags = ('--numeric-owner',) if location_config.get('numeric_ids') else () full_command = ( (local_path, 'extract') diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index bcc4b7450..990b96866 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -314,8 +314,8 @@ def make_parsers(): create_parser = subparsers.add_parser( 'create', aliases=SUBPARSER_ALIASES['create'], - help='Create archives (actually perform backups)', - description='Create archives (actually perform backups)', + help='Create an archive (actually perform a backup)', + description='Create an archive (actually perform a backup)', add_help=False, ) create_group = create_parser.add_argument_group('create arguments') diff --git a/borgmatic/config/generate.py b/borgmatic/config/generate.py index a454f662c..e864a3c0c 100644 --- a/borgmatic/config/generate.py +++ b/borgmatic/config/generate.py @@ -283,7 +283,7 @@ def generate_sample_configuration( if source_filename: source_config = load.load_configuration(source_filename) - normalize.normalize(source_config) + normalize.normalize(source_filename, source_config) destination_config = merge_source_configuration_into_destination( _schema_to_sample_configuration(schema), source_config diff --git a/borgmatic/config/normalize.py b/borgmatic/config/normalize.py index 3e57f538c..19cf5e9d8 100644 --- a/borgmatic/config/normalize.py +++ b/borgmatic/config/normalize.py @@ -36,12 +36,24 @@ def normalize(config_filename, config): if isinstance(checks, list) and len(checks) and isinstance(checks[0], str): config['consistency']['checks'] = [{'name': check_type} for check_type in checks] + # Rename various configuration options. + numeric_owner = config.get('location', {}).pop('numeric_owner', None) + if numeric_owner is not None: + config['location']['numeric_ids'] = numeric_owner + + bsd_flags = config.get('location', {}).pop('bsd_flags', None) + if bsd_flags is not None: + config['location']['flags'] = bsd_flags + + remote_rate_limit = config.get('storage', {}).pop('remote_rate_limit', None) + if remote_rate_limit is not None: + config['storage']['upload_rate_limit'] = remote_rate_limit + # Upgrade remote repositories to ssh:// syntax, required in Borg 2. repositories = config.get('location', {}).get('repositories') if repositories: config['location']['repositories'] = [] for repository in repositories: - # TODO: Instead of logging directly here, return logs and bubble them up to be displayed *after* logging is initialized. if '~' in repository: logs.append( logging.makeLogRecord( diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index 83c163d4f..2434eccd0 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -58,7 +58,7 @@ properties: database hook is used, the setting here is ignored and one_file_system is considered true. example: true - numeric_owner: + numeric_ids: type: boolean description: | Only store/extract numeric user and group identifiers. @@ -90,10 +90,10 @@ properties: used, the setting here is ignored and read_special is considered true. example: false - bsd_flags: + flags: type: boolean description: | - Record bsdflags (e.g. NODUMP, IMMUTABLE) in archive. + Record filesystem flags (e.g. NODUMP, IMMUTABLE) in archive. Defaults to true. example: true files_cache: @@ -255,7 +255,7 @@ properties: http://borgbackup.readthedocs.io/en/stable/usage/create.html for details. Defaults to "lz4". example: lz4 - remote_rate_limit: + upload_rate_limit: type: integer description: | Remote network upload rate limit in kiBytes/second. Defaults diff --git a/tests/unit/borg/test_create.py b/tests/unit/borg/test_create.py index 163000679..453dbf427 100644 --- a/tests/unit/borg/test_create.py +++ b/tests/unit/borg/test_create.py @@ -794,7 +794,7 @@ def test_create_archive_with_compression_calls_borg_with_compression_parameters( @pytest.mark.parametrize( 'feature_available,option_flag', ((True, '--upload-ratelimit'), (False, '--remote-ratelimit')), ) -def test_create_archive_with_remote_rate_limit_calls_borg_with_upload_ratelimit_parameters( +def test_create_archive_with_upload_rate_limit_calls_borg_with_upload_ratelimit_parameters( feature_available, option_flag ): flexmock(module).should_receive('borgmatic_source_directories').and_return([]) @@ -829,7 +829,7 @@ def test_create_archive_with_remote_rate_limit_calls_borg_with_upload_ratelimit_ 'repositories': ['repo'], 'exclude_patterns': None, }, - storage_config={'remote_rate_limit': 100}, + storage_config={'upload_rate_limit': 100}, local_borg_version='1.2.3', ) @@ -917,7 +917,7 @@ def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_par @pytest.mark.parametrize( 'feature_available,option_flag', ((True, '--numeric-ids'), (False, '--numeric-owner')), ) -def test_create_archive_with_numeric_owner_calls_borg_with_numeric_ids_parameter( +def test_create_archive_with_numeric_ids_calls_borg_with_numeric_ids_parameter( feature_available, option_flag ): flexmock(module).should_receive('borgmatic_source_directories').and_return([]) @@ -950,7 +950,7 @@ def test_create_archive_with_numeric_owner_calls_borg_with_numeric_ids_parameter location_config={ 'source_directories': ['foo', 'bar'], 'repositories': ['repo'], - 'numeric_owner': True, + 'numeric_ids': True, 'exclude_patterns': None, }, storage_config={}, @@ -1102,7 +1102,7 @@ def test_create_archive_with_atime_option_calls_borg_with_corresponding_paramete (False, False, '--nobsdflags'), ), ) -def test_create_archive_with_bsd_flags_option_calls_borg_with_corresponding_parameter( +def test_create_archive_with_flags_option_calls_borg_with_corresponding_parameter( option_value, feature_available, option_flag ): flexmock(module).should_receive('borgmatic_source_directories').and_return([]) @@ -1135,7 +1135,7 @@ def test_create_archive_with_bsd_flags_option_calls_borg_with_corresponding_para location_config={ 'source_directories': ['foo', 'bar'], 'repositories': ['repo'], - 'bsd_flags': option_value, + 'flags': option_value, 'exclude_patterns': None, }, storage_config={}, diff --git a/tests/unit/borg/test_extract.py b/tests/unit/borg/test_extract.py index 8e54dbc03..d572ff9e4 100644 --- a/tests/unit/borg/test_extract.py +++ b/tests/unit/borg/test_extract.py @@ -174,7 +174,7 @@ def test_extract_archive_calls_borg_with_numeric_ids_parameter(feature_available repository='repo', archive='archive', paths=None, - location_config={'numeric_owner': True}, + location_config={'numeric_ids': True}, storage_config={}, local_borg_version='1.2.3', ) diff --git a/tests/unit/config/test_normalize.py b/tests/unit/config/test_normalize.py index dfb41bb8f..93508ecc2 100644 --- a/tests/unit/config/test_normalize.py +++ b/tests/unit/config/test_normalize.py @@ -51,6 +51,13 @@ from borgmatic.config import normalize as module {'consistency': {'checks': [{'name': 'archives'}]}}, False, ), + ({'location': {'numeric_owner': False}}, {'location': {'numeric_ids': False}}, False,), + ({'location': {'bsd_flags': False}}, {'location': {'flags': False}}, False,), + ( + {'storage': {'remote_rate_limit': False}}, + {'storage': {'upload_rate_limit': False}}, + False, + ), ( {'location': {'repositories': ['foo@bar:/repo']}}, {'location': {'repositories': ['ssh://foo@bar/repo']}}, From 89d201c8fffed6f01e11fc7851202e09a26e41b4 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Wed, 17 Aug 2022 21:54:00 -0700 Subject: [PATCH 29/38] Fleshing out NEWS for the Borg 2 changes. --- NEWS | 16 +++++++++------- borgmatic/borg/list.py | 4 ++-- borgmatic/config/normalize.py | 2 +- tests/unit/config/test_normalize.py | 4 ++-- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/NEWS b/NEWS index b20033c87..a0505cdd4 100644 --- a/NEWS +++ b/NEWS @@ -1,18 +1,20 @@ 1.7.0.dev0 * #557: Support for Borg 2 while still working with Borg 1. This includes new borgmatic actions like "rcreate" (replaces "init"), "rlist" (list archives in repository), and "rinfo" (show - repository info). If you install Borg 2, you'll need to manually "borg transfer" or "borgmatic - transfer" your existing Borg 1 repositories before use. See the Borg 2.0 changelog for more - information about Borg 2: https://www.borgbackup.org/releases/borg-2.0.html + repository info). For the most part, borgmatic tries to smooth over differences between Borg 1 + and 2 to make your upgrade process easier. However, there are still a few cases where Borg made + breaking changes, such as moving flags from "borg list" to "borg rlist". See the Borg 2.0 + changelog for more information (https://www.borgbackup.org/releases/borg-2.0.html). If you + install Borg 2, you'll need to manually "borg transfer" or "borgmatic transfer" your existing + Borg 1 repositories before use. * #557: Rename several configuration options to match Borg 2: "remote_rate_limit" is now "upload_rate_limit", "numeric_owner" is "numeric_ids", and "bsd_flags" is "flags". borgmatic still works with the old options. * #557: Remote repository paths without the "ssh://" syntax are deprecated but still supported for - now. However, remote repository paths containing "~" will no longer work. + now. Remote repository paths containing "~" are deprecated in borgmatic and no longer work in + Borg 2. * #557: Omitting the "--archive" flag on the "list" action is deprecated when using Borg 2. Use - the new "rlist" action instead. And when using the "--archive" or "--find" flags on the "list" - action with Borg 2, several flags are no longer supported: "--prefix", "--glob-archives", - "--sort-by", "--first", and "--last". + the new "rlist" action instead. * #565: Fix handling of "repository" and "data" consistency checks to prevent invalid Borg flags. * #566: Modify "mount" and "extract" actions to require the "--repository" flag when multiple repositories are configured. diff --git a/borgmatic/borg/list.py b/borgmatic/borg/list.py index 3c2addb24..a13b943bc 100644 --- a/borgmatic/borg/list.py +++ b/borgmatic/borg/list.py @@ -95,7 +95,7 @@ def list_archive( if not list_arguments.archive and not list_arguments.find_paths: if feature.available(feature.Feature.RLIST, local_borg_version): logger.warning( - 'Omitting the --archive flag on the list action is deprecated when using Borg 2.x. Use the rlist action instead.' + 'Omitting the --archive flag on the list action is deprecated when using Borg 2.x+. Use the rlist action instead.' ) rlist_arguments = argparse.Namespace( @@ -117,7 +117,7 @@ def list_archive( for flag_name in ('prefix', 'glob-archives', 'sort-by', 'first', 'last'): if getattr(list_arguments, flag_name.replace('-', '_'), None): raise ValueError( - f'The --{flag_name} flag on the list action is not supported when using the --archive/--find flags and Borg 2.x.' + f'The --{flag_name} flag on the list action is not supported when using the --archive/--find flags and Borg 2.x+.' ) if list_arguments.json: diff --git a/borgmatic/config/normalize.py b/borgmatic/config/normalize.py index 19cf5e9d8..a43bb309b 100644 --- a/borgmatic/config/normalize.py +++ b/borgmatic/config/normalize.py @@ -60,7 +60,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.', + msg=f'{config_filename}: Repository paths containing "~" are deprecated in borgmatic and no longer work in Borg 2.x+.', ) ) ) diff --git a/tests/unit/config/test_normalize.py b/tests/unit/config/test_normalize.py index 93508ecc2..608824436 100644 --- a/tests/unit/config/test_normalize.py +++ b/tests/unit/config/test_normalize.py @@ -74,8 +74,8 @@ from borgmatic.config import normalize as module True, ), ( - {'location': {'repositories': ['ssh://foo@bar/repo']}}, - {'location': {'repositories': ['ssh://foo@bar/repo']}}, + {'location': {'repositories': ['ssh://foo@bar:1234/repo']}}, + {'location': {'repositories': ['ssh://foo@bar:1234/repo']}}, False, ), ), From b40e9b7da20471bc31c98bb4b5596353c5987d29 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Thu, 18 Aug 2022 09:59:48 -0700 Subject: [PATCH 30/38] Ignore archive filter parameters passed to list action when --archive is given (#557). --- NEWS | 7 +- borgmatic/borg/list.py | 25 +++-- docs/how-to/set-up-backups.md | 3 + tests/unit/borg/test_list.py | 189 +++++++++++++++++++++++++++------- 4 files changed, 177 insertions(+), 47 deletions(-) diff --git a/NEWS b/NEWS index a0505cdd4..f22d85631 100644 --- a/NEWS +++ b/NEWS @@ -3,10 +3,9 @@ like "rcreate" (replaces "init"), "rlist" (list archives in repository), and "rinfo" (show repository info). For the most part, borgmatic tries to smooth over differences between Borg 1 and 2 to make your upgrade process easier. However, there are still a few cases where Borg made - breaking changes, such as moving flags from "borg list" to "borg rlist". See the Borg 2.0 - changelog for more information (https://www.borgbackup.org/releases/borg-2.0.html). If you - install Borg 2, you'll need to manually "borg transfer" or "borgmatic transfer" your existing - Borg 1 repositories before use. + breaking changes. See the Borg 2.0 changelog for more information + (https://www.borgbackup.org/releases/borg-2.0.html). If you install Borg 2, you'll need to + manually "borg transfer" or "borgmatic transfer" your existing Borg 1 repositories before use. * #557: Rename several configuration options to match Borg 2: "remote_rate_limit" is now "upload_rate_limit", "numeric_owner" is "numeric_ids", and "bsd_flags" is "flags". borgmatic still works with the old options. diff --git a/borgmatic/borg/list.py b/borgmatic/borg/list.py index a13b943bc..360f5979b 100644 --- a/borgmatic/borg/list.py +++ b/borgmatic/borg/list.py @@ -9,7 +9,14 @@ from borgmatic.execute import execute_command logger = logging.getLogger(__name__) -MAKE_FLAGS_EXCLUDES = ('repository', 'archive', 'successful', 'paths', 'find_paths') +ARCHIVE_FILTER_FLAGS_MOVED_TO_RLIST = ('prefix', 'glob_archives', 'sort_by', 'first', 'last') +MAKE_FLAGS_EXCLUDES = ( + 'repository', + 'archive', + 'successful', + 'paths', + 'find_paths', +) + ARCHIVE_FILTER_FLAGS_MOVED_TO_RLIST def make_list_command( @@ -113,11 +120,11 @@ def list_archive( repository, storage_config, local_borg_version, rlist_arguments, local_path, remote_path ) - if feature.available(feature.Feature.RLIST, local_borg_version): - for flag_name in ('prefix', 'glob-archives', 'sort-by', 'first', 'last'): - if getattr(list_arguments, flag_name.replace('-', '_'), None): - raise ValueError( - f'The --{flag_name} flag on the list action is not supported when using the --archive/--find flags and Borg 2.x+.' + if list_arguments.archive: + for name in ARCHIVE_FILTER_FLAGS_MOVED_TO_RLIST: + if getattr(list_arguments, name, None): + logger.warning( + f"The --{name.replace('_', '-')} flag on the list action is ignored when using the --archive flag." ) if list_arguments.json: @@ -169,6 +176,12 @@ def list_archive( archive_arguments = copy.copy(list_arguments) archive_arguments.archive = archive + + # This list call is to show the files in a single archive, not list multiple archives. So + # blank out any archive filtering flags. They'll break anyway in Borg 2. + for name in ARCHIVE_FILTER_FLAGS_MOVED_TO_RLIST: + setattr(archive_arguments, name, None) + main_command = make_list_command( repository, storage_config, diff --git a/docs/how-to/set-up-backups.md b/docs/how-to/set-up-backups.md index 6508cd69a..e1a58ae51 100644 --- a/docs/how-to/set-up-backups.md +++ b/docs/how-to/set-up-backups.md @@ -204,6 +204,9 @@ Or, with Borg 2.x: sudo borgmatic rcreate --encryption repokey-aes-ocb ``` +(Note that `repokey-chacha20-poly1305` may be faster than `repokey-aes-ocb` on +certain platforms like ARM64.) + This uses the borgmatic configuration file you created above to determine which local or remote repository to create, and encrypts it with the encryption passphrase specified there if one is provided. Read about [Borg diff --git a/tests/unit/borg/test_list.py b/tests/unit/borg/test_list.py index d1c5d397b..0019d9798 100644 --- a/tests/unit/borg/test_list.py +++ b/tests/unit/borg/test_list.py @@ -254,7 +254,17 @@ def test_make_find_paths_adds_globs_to_path_fragments(): def test_list_archive_calls_borg_with_parameters(): - list_arguments = argparse.Namespace(archive='archive', paths=None, json=False, find_paths=None) + list_arguments = argparse.Namespace( + archive='archive', + paths=None, + json=False, + find_paths=None, + prefix=None, + glob_archives=None, + sort_by=None, + first=None, + last=None, + ) flexmock(module.feature).should_receive('available').and_return(False) flexmock(module).should_receive('make_list_command').with_args( @@ -297,7 +307,17 @@ def test_list_archive_with_archive_and_json_errors(): def test_list_archive_calls_borg_with_local_path(): - list_arguments = argparse.Namespace(archive='archive', paths=None, json=False, find_paths=None) + list_arguments = argparse.Namespace( + archive='archive', + paths=None, + json=False, + find_paths=None, + prefix=None, + glob_archives=None, + sort_by=None, + first=None, + last=None, + ) flexmock(module.feature).should_receive('available').and_return(False) flexmock(module).should_receive('make_list_command').with_args( @@ -346,9 +366,7 @@ def test_list_archive_calls_borg_multiple_times_with_find_paths(): output_log_level=None, borg_local_path='borg', extra_environment=None, - ).and_return( - 'archive1 Sun, 2022-05-29 15:27:04 [abc]\narchive2 Mon, 2022-05-30 19:47:15 [xyz]' - ).once() + ).and_return('archive1\narchive2').once() flexmock(module).should_receive('make_list_command').and_return( ('borg', 'list', 'repo::archive1') ).and_return(('borg', 'list', 'repo::archive2')) @@ -376,7 +394,17 @@ def test_list_archive_calls_borg_multiple_times_with_find_paths(): def test_list_archive_calls_borg_with_archive(): - list_arguments = argparse.Namespace(archive='archive', paths=None, json=False, find_paths=None) + list_arguments = argparse.Namespace( + archive='archive', + paths=None, + json=False, + find_paths=None, + prefix=None, + glob_archives=None, + sort_by=None, + first=None, + last=None, + ) flexmock(module.feature).should_receive('available').and_return(False) flexmock(module).should_receive('make_list_command').with_args( @@ -461,35 +489,15 @@ def test_list_archive_with_borg_features_without_archive_delegates_to_list_repos @pytest.mark.parametrize( 'archive_filter_flag', ('prefix', 'glob_archives', 'sort_by', 'first', 'last',), ) -def test_list_archive_with_archive_disallows_archive_filter_flag_if_rlist_feature_available( - archive_filter_flag, -): - list_arguments = argparse.Namespace( - archive='archive', paths=None, json=False, find_paths=None, **{archive_filter_flag: 'foo'} - ) - - flexmock(module.feature).should_receive('available').with_args( - module.feature.Feature.RLIST, '1.2.3' - ).and_return(True) - - with pytest.raises(ValueError): - module.list_archive( - repository='repo', - storage_config={}, - local_borg_version='1.2.3', - list_arguments=list_arguments, - ) - - -@pytest.mark.parametrize( - 'archive_filter_flag', ('prefix', 'glob_archives', 'sort_by', 'first', 'last',), -) -def test_list_archive_with_archive_allows_archive_filter_flag_if_rlist_feature_unavailable( - archive_filter_flag, -): - list_arguments = argparse.Namespace( - archive='archive', paths=None, json=False, find_paths=None, **{archive_filter_flag: 'foo'} - ) +def test_list_archive_with_archive_ignores_archive_filter_flag(archive_filter_flag,): + default_filter_flags = { + 'prefix': None, + 'glob_archives': None, + 'sort_by': None, + 'first': None, + 'last': None, + } + altered_filter_flags = {**default_filter_flags, **{archive_filter_flag: 'foo'}} flexmock(module.feature).should_receive('available').with_args( module.feature.Feature.RLIST, '1.2.3' @@ -498,7 +506,9 @@ def test_list_archive_with_archive_allows_archive_filter_flag_if_rlist_feature_u repository='repo', storage_config={}, local_borg_version='1.2.3', - list_arguments=list_arguments, + list_arguments=argparse.Namespace( + archive='archive', paths=None, json=False, find_paths=None, **default_filter_flags + ), local_path='borg', remote_path=None, ).and_return(('borg', 'list', 'repo::archive')) @@ -515,5 +525,110 @@ def test_list_archive_with_archive_allows_archive_filter_flag_if_rlist_feature_u repository='repo', storage_config={}, local_borg_version='1.2.3', - list_arguments=list_arguments, + list_arguments=argparse.Namespace( + archive='archive', paths=None, json=False, find_paths=None, **altered_filter_flags + ), + ) + + +@pytest.mark.parametrize( + 'archive_filter_flag', ('prefix', 'glob_archives', 'sort_by', 'first', 'last',), +) +def test_list_archive_with_find_paths_allows_archive_filter_flag_but_only_passes_it_to_rlist( + archive_filter_flag, +): + default_filter_flags = { + 'prefix': None, + 'glob_archives': None, + 'sort_by': None, + 'first': None, + 'last': None, + } + altered_filter_flags = {**default_filter_flags, **{archive_filter_flag: 'foo'}} + glob_paths = ('**/*foo.txt*/**',) + flexmock(module.feature).should_receive('available').and_return(True) + + flexmock(module.rlist).should_receive('make_rlist_command').with_args( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=argparse.Namespace( + repository='repo', short=True, format=None, json=None, **altered_filter_flags + ), + local_path='borg', + remote_path=None, + ).and_return(('borg', 'rlist', '--repo', 'repo')) + + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'rlist', '--repo', 'repo'), + output_log_level=None, + borg_local_path='borg', + extra_environment=None, + ).and_return('archive1\narchive2').once() + + flexmock(module).should_receive('make_list_command').with_args( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=argparse.Namespace( + repository='repo', + archive='archive1', + paths=None, + short=True, + format=None, + json=None, + find_paths=['foo.txt'], + **default_filter_flags, + ), + local_path='borg', + remote_path=None, + ).and_return(('borg', 'list', '--repo', 'repo', 'archive1')) + + flexmock(module).should_receive('make_list_command').with_args( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=argparse.Namespace( + repository='repo', + archive='archive2', + paths=None, + short=True, + format=None, + json=None, + find_paths=['foo.txt'], + **default_filter_flags, + ), + local_path='borg', + remote_path=None, + ).and_return(('borg', 'list', '--repo', 'repo', 'archive2')) + + flexmock(module).should_receive('make_find_paths').and_return(glob_paths) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'list', '--repo', 'repo', 'archive1') + glob_paths, + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ).once() + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'list', '--repo', 'repo', 'archive2') + glob_paths, + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ).once() + + module.list_archive( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=argparse.Namespace( + repository='repo', + archive=None, + paths=None, + short=True, + format=None, + json=None, + find_paths=['foo.txt'], + **altered_filter_flags, + ), ) From 00255a2437353077055dbc9e7b267229182e5a38 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Thu, 18 Aug 2022 10:19:11 -0700 Subject: [PATCH 31/38] Various documentation edits for Borg 2 (#557). --- docs/how-to/extract-a-backup.md | 24 +++++++++++++----------- docs/how-to/monitor-your-backups.md | 4 ++-- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/docs/how-to/extract-a-backup.md b/docs/how-to/extract-a-backup.md index a8ae9c544..62bdc75b1 100644 --- a/docs/how-to/extract-a-backup.md +++ b/docs/how-to/extract-a-backup.md @@ -31,10 +31,9 @@ and therefore the latest timestamp, run a command like: borgmatic extract --archive host-2019-01-02T04:06:07.080910 ``` -(No borgmatic `extract` action? Try the old-style `--extract`, or upgrade -borgmatic!) +(No borgmatic `extract` action? Upgrade borgmatic!) -With newer versions of borgmatic, you can simplify this to: +Or simplify this to: ```bash borgmatic extract --archive latest @@ -42,7 +41,8 @@ borgmatic extract --archive latest The `--archive` value is the name of the archive to extract. This extracts the entire contents of the archive to the current directory, so make sure you're -in the right place before running the command. +in the right place before running the command—or see below about the +`--destination` flag. ## Repository selection @@ -64,13 +64,15 @@ everything from an archive. To do that, tack on one or more `--path` values. For instance: ```bash -borgmatic extract --archive host-2019-... --path path/1 path/2 +borgmatic extract --archive latest --path path/1 path/2 ``` Note that the specified restore paths should not have a leading slash. Like a -whole-archive extract, this also extracts into the current directory. So for -example, if you happen to be in the directory `/var` and you run the `extract` -command above, borgmatic will extract `/var/path/1` and `/var/path/2`. +whole-archive extract, this also extracts into the current directory by +default. So for example, if you happen to be in the directory `/var` and you +run the `extract` command above, borgmatic will extract `/var/path/1` and +`/var/path/2`. + ## Extract to a particular destination @@ -79,7 +81,7 @@ extract files to a particular destination directory, use the `--destination` flag: ```bash -borgmatic extract --archive host-2019-... --destination /tmp +borgmatic extract --archive latest --destination /tmp ``` When using the `--destination` flag, be careful not to overwrite your system's @@ -103,7 +105,7 @@ archive as a [FUSE](https://en.wikipedia.org/wiki/Filesystem_in_Userspace) filesystem, you can use the `borgmatic mount` action. Here's an example: ```bash -borgmatic mount --archive host-2019-... --mount-point /mnt +borgmatic mount --archive latest --mount-point /mnt ``` This mounts the entire archive on the given mount point `/mnt`, so that you @@ -126,7 +128,7 @@ your archive, use the `--path` flag, similar to the `extract` action above. For instance: ```bash -borgmatic mount --archive host-2019-... --mount-point /mnt --path var/lib +borgmatic mount --archive latest --mount-point /mnt --path var/lib ``` When you're all done exploring your files, unmount your mount point. No diff --git a/docs/how-to/monitor-your-backups.md b/docs/how-to/monitor-your-backups.md index f8c21bb26..7e62dac7a 100644 --- a/docs/how-to/monitor-your-backups.md +++ b/docs/how-to/monitor-your-backups.md @@ -319,8 +319,8 @@ hooks: ## Scripting borgmatic To consume the output of borgmatic in other software, you can include an -optional `--json` flag with `create`, `list`, or `info` to get the output -formatted as JSON. +optional `--json` flag with `create`, `rlist`, `rinfo`, or `info` to get the +output formatted as JSON. Note that when you specify the `--json` flag, Borg's other non-JSON output is suppressed so as not to interfere with the captured JSON. Also note that JSON From 43d711463cffcac9b1e535ee0470e34e618623da Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Thu, 18 Aug 2022 14:28:12 -0700 Subject: [PATCH 32/38] Add additional command-line flags to rcreate action (#557). --- borgmatic/borg/rcreate.py | 8 +++-- borgmatic/commands/arguments.py | 19 +++++++---- borgmatic/commands/borgmatic.py | 4 ++- tests/unit/borg/test_rcreate.py | 46 ++++++++++++++++++++++----- tests/unit/commands/test_borgmatic.py | 4 ++- 5 files changed, 63 insertions(+), 18 deletions(-) diff --git a/borgmatic/borg/rcreate.py b/borgmatic/borg/rcreate.py index 6babe0170..81be86465 100644 --- a/borgmatic/borg/rcreate.py +++ b/borgmatic/borg/rcreate.py @@ -16,9 +16,11 @@ def create_repository( storage_config, local_borg_version, encryption_mode, - other_repo=None, + key_repository=None, + copy_crypt_key=False, append_only=None, storage_quota=None, + make_parent_dirs=False, local_path='borg', remote_path=None, ): @@ -53,9 +55,11 @@ def create_repository( else ('init',) ) + (('--encryption', encryption_mode) if encryption_mode else ()) - + (('--other-repo', other_repo) if other_repo else ()) + + (('--other-repo', key_repository) if key_repository else ()) + + (('--copy-crypt-key',) if copy_crypt_key else ()) + (('--append-only',) if append_only else ()) + (('--storage-quota', storage_quota) if storage_quota else ()) + + (('--make-parent-dirs',) if make_parent_dirs else ()) + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ()) + (('--debug',) if logger.isEnabledFor(logging.DEBUG) else ()) + (('--remote-path', remote_path) if remote_path else ()) diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index 990b96866..a5b8afcfc 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -18,6 +18,7 @@ SUBPARSER_ALIASES = { 'list': ['--list', '-l'], 'rinfo': [], 'info': ['--info', '-i'], + 'transfer': [], 'borg': [], } @@ -240,20 +241,26 @@ def make_parsers(): required=True, ) rcreate_group.add_argument( + '--key-repository', '--other-repo', metavar='SOURCE_REPOSITORY', help='Path to an existing Borg repository whose key material should be reused (Borg 2.x+ only)', ) rcreate_group.add_argument( - '--append-only', - dest='append_only', + '--copy-crypt-key', action='store_true', - help='Create an append-only repository', + help='Copy the crypt key used for authenticated encryption from the key repository, defaults to a new random key (Borg 2.x+ only)', ) rcreate_group.add_argument( - '--storage-quota', - dest='storage_quota', - help='Create a repository with a fixed storage quota', + '--append-only', action='store_true', help='Create an append-only repository', + ) + rcreate_group.add_argument( + '--storage-quota', help='Create a repository with a fixed storage quota', + ) + rcreate_group.add_argument( + '--make-parent-dirs', + action='store_true', + help='Create any missing parent directories of the repository directory', ) rcreate_group.add_argument( '-h', '--help', action='help', help='Show this help message and exit' diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index 343b9a64d..a905713c2 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -258,9 +258,11 @@ def run_actions( storage, local_borg_version, arguments['rcreate'].encryption_mode, - arguments['rcreate'].other_repo, + arguments['rcreate'].key_repository, + arguments['rcreate'].copy_crypt_key, arguments['rcreate'].append_only, arguments['rcreate'].storage_quota, + arguments['rcreate'].make_parent_dirs, local_path=local_path, remote_path=remote_path, ) diff --git a/tests/unit/borg/test_rcreate.py b/tests/unit/borg/test_rcreate.py index 1a22e8e01..18166a9ff 100644 --- a/tests/unit/borg/test_rcreate.py +++ b/tests/unit/borg/test_rcreate.py @@ -32,7 +32,7 @@ def insert_rcreate_command_mock(rcreate_command, **kwargs): ).once() -def test_create_repository_calls_borg_with_parameters(): +def test_create_repository_calls_borg_with_flags(): insert_rinfo_command_not_found_mock() insert_rcreate_command_mock(RCREATE_COMMAND + ('--repo', 'repo')) flexmock(module.feature).should_receive('available').and_return(True) @@ -85,7 +85,7 @@ def test_create_repository_raises_for_unknown_rinfo_command_error(): ) -def test_create_repository_with_append_only_calls_borg_with_other_repo_parameter(): +def test_create_repository_with_key_repository_calls_borg_with_other_repo_flag(): insert_rinfo_command_not_found_mock() insert_rcreate_command_mock(RCREATE_COMMAND + ('--other-repo', 'other.borg', '--repo', 'repo')) flexmock(module.feature).should_receive('available').and_return(True) @@ -96,11 +96,26 @@ def test_create_repository_with_append_only_calls_borg_with_other_repo_parameter storage_config={}, local_borg_version='2.3.4', encryption_mode='repokey', - other_repo='other.borg', + key_repository='other.borg', ) -def test_create_repository_with_append_only_calls_borg_with_append_only_parameter(): +def test_create_repository_with_copy_crypt_key_calls_borg_with_copy_crypt_key_flag(): + insert_rinfo_command_not_found_mock() + insert_rcreate_command_mock(RCREATE_COMMAND + ('--copy-crypt-key', '--repo', 'repo')) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + + module.create_repository( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', + copy_crypt_key=True, + ) + + +def test_create_repository_with_append_only_calls_borg_with_append_only_flag(): insert_rinfo_command_not_found_mock() insert_rcreate_command_mock(RCREATE_COMMAND + ('--append-only', '--repo', 'repo')) flexmock(module.feature).should_receive('available').and_return(True) @@ -115,7 +130,7 @@ def test_create_repository_with_append_only_calls_borg_with_append_only_paramete ) -def test_create_repository_with_storage_quota_calls_borg_with_storage_quota_parameter(): +def test_create_repository_with_storage_quota_calls_borg_with_storage_quota_flag(): insert_rinfo_command_not_found_mock() insert_rcreate_command_mock(RCREATE_COMMAND + ('--storage-quota', '5G', '--repo', 'repo')) flexmock(module.feature).should_receive('available').and_return(True) @@ -130,7 +145,22 @@ def test_create_repository_with_storage_quota_calls_borg_with_storage_quota_para ) -def test_create_repository_with_log_info_calls_borg_with_info_parameter(): +def test_create_repository_with_make_parent_dirs_calls_borg_with_make_parent_dirs_flag(): + insert_rinfo_command_not_found_mock() + insert_rcreate_command_mock(RCREATE_COMMAND + ('--make-parent-dirs', '--repo', 'repo')) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + + module.create_repository( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', + make_parent_dirs=True, + ) + + +def test_create_repository_with_log_info_calls_borg_with_info_flag(): insert_rinfo_command_not_found_mock() insert_rcreate_command_mock(RCREATE_COMMAND + ('--info', '--repo', 'repo')) insert_logging_mock(logging.INFO) @@ -142,7 +172,7 @@ def test_create_repository_with_log_info_calls_borg_with_info_parameter(): ) -def test_create_repository_with_log_debug_calls_borg_with_debug_parameter(): +def test_create_repository_with_log_debug_calls_borg_with_debug_flag(): insert_rinfo_command_not_found_mock() insert_rcreate_command_mock(RCREATE_COMMAND + ('--debug', '--repo', 'repo')) insert_logging_mock(logging.DEBUG) @@ -169,7 +199,7 @@ def test_create_repository_with_local_path_calls_borg_via_local_path(): ) -def test_create_repository_with_remote_path_calls_borg_with_remote_path_parameter(): +def test_create_repository_with_remote_path_calls_borg_with_remote_path_flag(): insert_rinfo_command_not_found_mock() insert_rcreate_command_mock(RCREATE_COMMAND + ('--remote-path', 'borg1', '--repo', 'repo')) flexmock(module.feature).should_receive('available').and_return(True) diff --git a/tests/unit/commands/test_borgmatic.py b/tests/unit/commands/test_borgmatic.py index 670377e7f..4b9c43ea9 100644 --- a/tests/unit/commands/test_borgmatic.py +++ b/tests/unit/commands/test_borgmatic.py @@ -346,9 +346,11 @@ def test_run_actions_does_not_raise_for_rcreate_action(): 'global': flexmock(monitoring_verbosity=1, dry_run=False), 'rcreate': flexmock( encryption_mode=flexmock(), - other_repo=flexmock(), + key_repository=flexmock(), + copy_crypt_key=flexmock(), append_only=flexmock(), storage_quota=flexmock(), + make_parent_dirs=flexmock(), ), } From 68f9c1b950d81c2626859856406120cfc902ab57 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Thu, 18 Aug 2022 14:28:46 -0700 Subject: [PATCH 33/38] Add generate-borgmatic-config end-to-end test. --- tests/end-to-end/test_generate_config.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 tests/end-to-end/test_generate_config.py diff --git a/tests/end-to-end/test_generate_config.py b/tests/end-to-end/test_generate_config.py new file mode 100644 index 000000000..b8cade96d --- /dev/null +++ b/tests/end-to-end/test_generate_config.py @@ -0,0 +1,16 @@ +import os +import subprocess +import tempfile + + +def test_generate_borgmatic_config_with_merging_succeeds(): + with tempfile.TemporaryDirectory() as temporary_directory: + config_path = os.path.join(temporary_directory, 'test.yaml') + new_config_path = os.path.join(temporary_directory, 'new.yaml') + + subprocess.check_call(f'generate-borgmatic-config --destination {config_path}'.split(' ')) + subprocess.check_call( + f'generate-borgmatic-config --source {config_path} --destination {new_config_path}'.split( + ' ' + ) + ) From 7dee6194a2d530a30ed38d587130c70bf4c1069e Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Thu, 18 Aug 2022 23:06:51 -0700 Subject: [PATCH 34/38] Add new "transfer" action for Borg 2 (#557). --- NEWS | 16 ++-- borgmatic/borg/compact.py | 17 ++-- borgmatic/borg/rcreate.py | 19 +++-- borgmatic/borg/transfer.py | 45 ++++++++++ borgmatic/commands/arguments.py | 65 +++++++++++++-- borgmatic/commands/borgmatic.py | 15 +++- docs/how-to/upgrade.md | 88 +++++++++++++++++++- tests/integration/commands/test_arguments.py | 9 -- tests/unit/borg/test_rcreate.py | 53 ++++++++++-- tests/unit/commands/test_borgmatic.py | 26 +++++- 10 files changed, 307 insertions(+), 46 deletions(-) create mode 100644 borgmatic/borg/transfer.py diff --git a/NEWS b/NEWS index f22d85631..274705389 100644 --- a/NEWS +++ b/NEWS @@ -1,11 +1,14 @@ 1.7.0.dev0 * #557: Support for Borg 2 while still working with Borg 1. This includes new borgmatic actions - like "rcreate" (replaces "init"), "rlist" (list archives in repository), and "rinfo" (show - repository info). For the most part, borgmatic tries to smooth over differences between Borg 1 - and 2 to make your upgrade process easier. However, there are still a few cases where Borg made - breaking changes. See the Borg 2.0 changelog for more information - (https://www.borgbackup.org/releases/borg-2.0.html). If you install Borg 2, you'll need to - manually "borg transfer" or "borgmatic transfer" your existing Borg 1 repositories before use. + like "rcreate" (replaces "init"), "rlist" (list archives in repository), "rinfo" (show repository + info), and "transfer" (for upgrading Borg repositories). For the most part, borgmatic tries to + smooth over differences between Borg 1 and 2 to make your upgrade process easier. However, there + are still a few cases where Borg made breaking changes. See the Borg 2.0 changelog for more + information: https://www.borgbackup.org/releases/borg-2.0.html + * #557: If you install Borg 2, you'll need to manually upgrade your existing Borg 1 repositories + before use. Note that Borg 2 stable is not yet released as of this borgmatic release, so don't + use Borg 2 for production until it is! See the documentation for more information: + https://torsion.org/borgmatic/docs/how-to/upgrade/#upgrading-borg * #557: Rename several configuration options to match Borg 2: "remote_rate_limit" is now "upload_rate_limit", "numeric_owner" is "numeric_ids", and "bsd_flags" is "flags". borgmatic still works with the old options. @@ -14,6 +17,7 @@ Borg 2. * #557: Omitting the "--archive" flag on the "list" action is deprecated when using Borg 2. Use the new "rlist" action instead. + * #557: The "--dry-run" flag can now be used with the "rcreate"/"init" action. * #565: Fix handling of "repository" and "data" consistency checks to prevent invalid Borg flags. * #566: Modify "mount" and "extract" actions to require the "--repository" flag when multiple repositories are configured. diff --git a/borgmatic/borg/compact.py b/borgmatic/borg/compact.py index c9770e093..847ed26b3 100644 --- a/borgmatic/borg/compact.py +++ b/borgmatic/borg/compact.py @@ -39,10 +39,13 @@ def compact_segments( + flags.make_repository_flags(repository, local_borg_version) ) - if not dry_run: - execute_command( - full_command, - output_log_level=logging.INFO, - borg_local_path=local_path, - extra_environment=environment.make_environment(storage_config), - ) + if dry_run: + logging.info(f'{repository}: Skipping compact (dry run)') + return + + execute_command( + full_command, + output_log_level=logging.INFO, + borg_local_path=local_path, + extra_environment=environment.make_environment(storage_config), + ) diff --git a/borgmatic/borg/rcreate.py b/borgmatic/borg/rcreate.py index 81be86465..d3a8f7aa3 100644 --- a/borgmatic/borg/rcreate.py +++ b/borgmatic/borg/rcreate.py @@ -12,11 +12,12 @@ RINFO_REPOSITORY_NOT_FOUND_EXIT_CODE = 2 def create_repository( + dry_run, repository, storage_config, local_borg_version, encryption_mode, - key_repository=None, + source_repository=None, copy_crypt_key=False, append_only=None, storage_quota=None, @@ -25,10 +26,10 @@ def create_repository( remote_path=None, ): ''' - Given a local or remote repository path, a storage configuration dict, the local Borg version, a - Borg encryption mode, the path to another repo whose key material should be reused, whether the - repository should be append-only, and the storage quota to use, create the repository. If the - repository already exists, then log and skip creation. + Given a dry-run flag, a local or remote repository path, a storage configuration dict, the local + Borg version, a Borg encryption mode, the path to another repo whose key material should be + reused, whether the repository should be append-only, and the storage quota to use, create the + repository. If the repository already exists, then log and skip creation. ''' try: rinfo.display_repository_info( @@ -39,7 +40,7 @@ def create_repository( local_path, remote_path, ) - logger.info('Repository already exists. Skipping creation.') + logger.info(f'{repository}: Repository already exists. Skipping creation.') return except subprocess.CalledProcessError as error: if error.returncode != RINFO_REPOSITORY_NOT_FOUND_EXIT_CODE: @@ -55,7 +56,7 @@ def create_repository( else ('init',) ) + (('--encryption', encryption_mode) if encryption_mode else ()) - + (('--other-repo', key_repository) if key_repository else ()) + + (('--other-repo', source_repository) if source_repository else ()) + (('--copy-crypt-key',) if copy_crypt_key else ()) + (('--append-only',) if append_only else ()) + (('--storage-quota', storage_quota) if storage_quota else ()) @@ -67,6 +68,10 @@ def create_repository( + flags.make_repository_flags(repository, local_borg_version) ) + if dry_run: + logging.info(f'{repository}: Skipping repository creation (dry run)') + return + # Do not capture output here, so as to support interactive prompts. execute_command( rcreate_command, diff --git a/borgmatic/borg/transfer.py b/borgmatic/borg/transfer.py new file mode 100644 index 000000000..8647fb9b9 --- /dev/null +++ b/borgmatic/borg/transfer.py @@ -0,0 +1,45 @@ +import logging + +from borgmatic.borg import environment, flags +from borgmatic.execute import execute_command + +logger = logging.getLogger(__name__) + + +def transfer_archives( + dry_run, + repository, + storage_config, + local_borg_version, + transfer_arguments, + local_path='borg', + remote_path=None, +): + ''' + Given a dry-run flag, a local or remote repository path, a storage config dict, the local Borg + version, and the arguments to the transfer action, transfer archives to the given repository. + ''' + full_command = ( + (local_path, 'transfer') + + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ()) + + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ()) + + flags.make_flags('remote-path', remote_path) + + flags.make_flags('lock-wait', storage_config.get('lock_wait', None)) + + flags.make_flags( + 'glob-archives', transfer_arguments.glob_archives or transfer_arguments.archive + ) + + flags.make_flags_from_arguments( + transfer_arguments, + excludes=('repository', 'source_repository', 'archive', 'glob_archives'), + ) + + flags.make_repository_flags(repository, local_borg_version) + + flags.make_flags('other-repo', transfer_arguments.source_repository) + + flags.make_flags('dry-run', dry_run) + ) + + return execute_command( + full_command, + output_log_level=logging.WARNING, + borg_local_path=local_path, + extra_environment=environment.make_environment(storage_config), + ) diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index a5b8afcfc..bb3ed4b7c 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -241,15 +241,15 @@ def make_parsers(): required=True, ) rcreate_group.add_argument( - '--key-repository', + '--source-repository', '--other-repo', - metavar='SOURCE_REPOSITORY', + metavar='KEY_REPOSITORY', help='Path to an existing Borg repository whose key material should be reused (Borg 2.x+ only)', ) rcreate_group.add_argument( '--copy-crypt-key', action='store_true', - help='Copy the crypt key used for authenticated encryption from the key repository, defaults to a new random key (Borg 2.x+ only)', + help='Copy the crypt key used for authenticated encryption from the source repository, defaults to a new random key (Borg 2.x+ only)', ) rcreate_group.add_argument( '--append-only', action='store_true', help='Create an append-only repository', @@ -266,6 +266,53 @@ def make_parsers(): '-h', '--help', action='help', help='Show this help message and exit' ) + transfer_parser = subparsers.add_parser( + 'transfer', + aliases=SUBPARSER_ALIASES['transfer'], + help='Transfer archives from one repository to another, optionally upgrading the transferred data', + description='Transfer archives from one repository to another, optionally upgrading the transferred data', + add_help=False, + ) + transfer_group = transfer_parser.add_argument_group('transfer arguments') + transfer_group.add_argument( + '--repository', + help='Path of existing destination repository to transfer archives to, defaults to the configured repository if there is only one', + ) + transfer_group.add_argument( + '--source-repository', + help='Path of existing source repository to transfer archives from', + required=True, + ) + transfer_group.add_argument( + '--archive', + help='Name of single archive to transfer (or "latest"), defaults to transferring all archives', + ) + transfer_group.add_argument( + '--upgrader', + help='Upgrader type used to convert the transfered data, e.g. "From12To20" to upgrade data from Borg 1.2 to 2.0 format, defaults to no conversion', + required=True, + ) + transfer_group.add_argument( + '-a', + '--glob-archives', + metavar='GLOB', + help='Only transfer archives with names matching this glob', + ) + transfer_group.add_argument( + '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys' + ) + transfer_group.add_argument( + '--first', + metavar='N', + help='Only transfer first N archives after other filters are applied', + ) + transfer_group.add_argument( + '--last', metavar='N', help='Only transfer last N archives after other filters are applied' + ) + transfer_group.add_argument( + '-h', '--help', action='help', help='Show this help message and exit' + ) + prune_parser = subparsers.add_parser( 'prune', aliases=SUBPARSER_ALIASES['prune'], @@ -760,9 +807,6 @@ def parse_arguments(*unparsed_arguments): 'The --excludes flag has been replaced with exclude_patterns in configuration.' ) - if 'rcreate' in arguments and arguments['global'].dry_run: - raise ValueError('The rcreate/init action cannot be used with the --dry-run flag.') - if ( ('list' in arguments and 'rinfo' in arguments and arguments['list'].json) or ('list' in arguments and 'info' in arguments and arguments['list'].json) @@ -770,6 +814,15 @@ def parse_arguments(*unparsed_arguments): ): raise ValueError('With the --json flag, multiple actions cannot be used together.') + if ( + 'transfer' in arguments + and arguments['transfer'].archive + and arguments['transfer'].glob_archives + ): + raise ValueError( + 'With the transfer action, only one of --archive and --glob-archives flags can be used.' + ) + if 'info' in arguments and ( (arguments['info'].archive and arguments['info'].prefix) or (arguments['info'].archive and arguments['info'].glob_archives) diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index a905713c2..c03fc4418 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -26,6 +26,7 @@ from borgmatic.borg import prune as borg_prune from borgmatic.borg import rcreate as borg_rcreate from borgmatic.borg import rinfo as borg_rinfo from borgmatic.borg import rlist as borg_rlist +from borgmatic.borg import transfer as borg_transfer from borgmatic.borg import umount as borg_umount from borgmatic.borg import version as borg_version from borgmatic.commands.arguments import parse_arguments @@ -254,11 +255,12 @@ def run_actions( if 'rcreate' in arguments: logger.info('{}: Creating repository'.format(repository)) borg_rcreate.create_repository( + global_arguments.dry_run, repository, storage, local_borg_version, arguments['rcreate'].encryption_mode, - arguments['rcreate'].key_repository, + arguments['rcreate'].source_repository, arguments['rcreate'].copy_crypt_key, arguments['rcreate'].append_only, arguments['rcreate'].storage_quota, @@ -266,6 +268,17 @@ def run_actions( local_path=local_path, remote_path=remote_path, ) + if 'transfer' in arguments: + logger.info(f'{repository}: Transferring archives to repository') + borg_transfer.transfer_archives( + global_arguments.dry_run, + repository, + storage, + local_borg_version, + transfer_arguments=arguments['transfer'], + local_path=local_path, + remote_path=remote_path, + ) if 'prune' in arguments: command.execute_hook( hooks.get('before_prune'), diff --git a/docs/how-to/upgrade.md b/docs/how-to/upgrade.md index cdcdefe5f..28955c49d 100644 --- a/docs/how-to/upgrade.md +++ b/docs/how-to/upgrade.md @@ -1,11 +1,11 @@ --- -title: How to upgrade borgmatic +title: How to upgrade borgmatic and Borg eleventyNavigation: - key: 📦 Upgrade borgmatic + key: 📦 Upgrade borgmatic/Borg parent: How-to guides order: 12 --- -## Upgrading +## Upgrading borgmatic In general, all you should need to do to upgrade borgmatic is run the following: @@ -115,3 +115,85 @@ sudo pip3 install --user borgmatic That's it! borgmatic will continue using your /etc/borgmatic configuration files. + + +## Upgrading Borg + +To upgrade to a new version of Borg, you can generally install a new version +the same way you installed the previous version, paying attention to any +instructions included with each Borg release changelog linked from the +[releases page](https://github.com/borgbackup/borg/releases). However, some +more major Borg releases require additional steps that borgmatic can help +with. + + +### Borg 1.2 to 2.0 + +New in borgmatic version 1.7.0 +Upgrading Borg from 1.2 to 2.0 requires manually upgrading your existing Borg +1 repositories before use with Borg or borgmatic. Here's how you can +accomplish that. + +Start by upgrading borgmatic as described above to at least version 1.7.0 and +Borg to 2.0. Then, rename your repository in borgmatic's configuration file to +a new repository path. The repository upgrade process does not occur +in-place; you'll create a new repository with a copy of your old repository's +data. + +Let's say your original borgmatic repository configuration file looks something +like this: + +```yaml +location: + repositories: + - original.borg +``` + +Change it to a new (not yet created) repository path: + +```yaml +location: + repositories: + - upgraded.borg +``` + +Then, run the `rcreate` action (formerly `init`) to create that new Borg 2 +repository: + +```bash +borgmatic rcreate --verbosity 1 --encryption repokey-aes-ocb \ + --source-repository original.borg --repository upgraded.borg +``` + +(Note that `repokey-chacha20-poly1305` may be faster than `repokey-aes-ocb` on +certain platforms like ARM64.) + +This creates an empty repository and doesn't actually transfer any data yet. +The `--source-repository` flag is necessary to reuse key material from your +Borg 1 repository so that the subsequent data transfer can work. + +To transfer data from your original Borg 1 repository to your newly created +Borg 2 repository: + +```bash +borgmatic transfer --verbosity 1 --upgrader From12To20 --source-repository \ + original.borg --repository upgraded.borg --dry-run +borgmatic transfer --verbosity 1 --upgrader From12To20 --source-repository \ + original.borg --repository upgraded.borg +borgmatic transfer --verbosity 1 --upgrader From12To20 --source-repository \ + original.borg --repository upgraded.borg --dry-run +``` + +The first command with `--dry-run` tells you what Borg is going to do during +the transfer, the second command actually performs the transfer/upgrade (this +might take a while), and the final command with `--dry-run` again provides +confirmation of success—or tells you if something hasn't been transferred yet. + +Note that by omitting the `--upgrader` flag, you can also do archive transfers +between Borg 2 repositories without upgrading, even down to individual +archives. For more on that functionality, see the [Borg transfer +documentation](https://borgbackup.readthedocs.io/en/2.0.0b1/usage/transfer.html). + +That's it! Now you can use your new Borg 2 repository as normal with +borgmatic. If you've got multiple repositories, repeat the above process for +each. diff --git a/tests/integration/commands/test_arguments.py b/tests/integration/commands/test_arguments.py index b70135630..eb2a1f9ed 100644 --- a/tests/integration/commands/test_arguments.py +++ b/tests/integration/commands/test_arguments.py @@ -287,15 +287,6 @@ def test_parse_arguments_allows_init_and_create(): module.parse_arguments('--config', 'myconfig', 'init', '--encryption', 'repokey', 'create') -def test_parse_arguments_disallows_init_and_dry_run(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - with pytest.raises(ValueError): - module.parse_arguments( - '--config', 'myconfig', 'init', '--encryption', 'repokey', '--dry-run' - ) - - def test_parse_arguments_disallows_repository_unless_action_consumes_it(): flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) diff --git a/tests/unit/borg/test_rcreate.py b/tests/unit/borg/test_rcreate.py index 18166a9ff..612ec11c1 100644 --- a/tests/unit/borg/test_rcreate.py +++ b/tests/unit/borg/test_rcreate.py @@ -39,7 +39,26 @@ def test_create_repository_calls_borg_with_flags(): flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) module.create_repository( - repository='repo', storage_config={}, local_borg_version='2.3.4', encryption_mode='repokey' + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', + ) + + +def test_create_repository_with_dry_run_skips_borg_call(): + insert_rinfo_command_not_found_mock() + flexmock(module).should_receive('execute_command').never() + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + + module.create_repository( + dry_run=True, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', ) @@ -54,6 +73,7 @@ def test_create_repository_raises_for_borg_rcreate_error(): with pytest.raises(subprocess.CalledProcessError): module.create_repository( + dry_run=False, repository='repo', storage_config={}, local_borg_version='2.3.4', @@ -67,7 +87,11 @@ def test_create_repository_skips_creation_when_repository_already_exists(): flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) module.create_repository( - repository='repo', storage_config={}, local_borg_version='2.3.4', encryption_mode='repokey' + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', ) @@ -78,6 +102,7 @@ def test_create_repository_raises_for_unknown_rinfo_command_error(): with pytest.raises(subprocess.CalledProcessError): module.create_repository( + dry_run=False, repository='repo', storage_config={}, local_borg_version='2.3.4', @@ -85,18 +110,19 @@ def test_create_repository_raises_for_unknown_rinfo_command_error(): ) -def test_create_repository_with_key_repository_calls_borg_with_other_repo_flag(): +def test_create_repository_with_source_repository_calls_borg_with_other_repo_flag(): insert_rinfo_command_not_found_mock() insert_rcreate_command_mock(RCREATE_COMMAND + ('--other-repo', 'other.borg', '--repo', 'repo')) flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) module.create_repository( + dry_run=False, repository='repo', storage_config={}, local_borg_version='2.3.4', encryption_mode='repokey', - key_repository='other.borg', + source_repository='other.borg', ) @@ -107,6 +133,7 @@ def test_create_repository_with_copy_crypt_key_calls_borg_with_copy_crypt_key_fl flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) module.create_repository( + dry_run=False, repository='repo', storage_config={}, local_borg_version='2.3.4', @@ -122,6 +149,7 @@ def test_create_repository_with_append_only_calls_borg_with_append_only_flag(): flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) module.create_repository( + dry_run=False, repository='repo', storage_config={}, local_borg_version='2.3.4', @@ -137,6 +165,7 @@ def test_create_repository_with_storage_quota_calls_borg_with_storage_quota_flag flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) module.create_repository( + dry_run=False, repository='repo', storage_config={}, local_borg_version='2.3.4', @@ -152,6 +181,7 @@ def test_create_repository_with_make_parent_dirs_calls_borg_with_make_parent_dir flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) module.create_repository( + dry_run=False, repository='repo', storage_config={}, local_borg_version='2.3.4', @@ -168,7 +198,11 @@ def test_create_repository_with_log_info_calls_borg_with_info_flag(): flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) module.create_repository( - repository='repo', storage_config={}, local_borg_version='2.3.4', encryption_mode='repokey' + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', ) @@ -180,7 +214,11 @@ def test_create_repository_with_log_debug_calls_borg_with_debug_flag(): flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) module.create_repository( - repository='repo', storage_config={}, local_borg_version='2.3.4', encryption_mode='repokey' + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', ) @@ -191,6 +229,7 @@ def test_create_repository_with_local_path_calls_borg_via_local_path(): flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) module.create_repository( + dry_run=False, repository='repo', storage_config={}, local_borg_version='2.3.4', @@ -206,6 +245,7 @@ def test_create_repository_with_remote_path_calls_borg_with_remote_path_flag(): flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) module.create_repository( + dry_run=False, repository='repo', storage_config={}, local_borg_version='2.3.4', @@ -221,6 +261,7 @@ def test_create_repository_with_extra_borg_options_calls_borg_with_extra_options flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) module.create_repository( + dry_run=False, repository='repo', storage_config={'extra_borg_options': {'rcreate': '--extra --options'}}, local_borg_version='2.3.4', diff --git a/tests/unit/commands/test_borgmatic.py b/tests/unit/commands/test_borgmatic.py index 4b9c43ea9..5a49aff8a 100644 --- a/tests/unit/commands/test_borgmatic.py +++ b/tests/unit/commands/test_borgmatic.py @@ -346,7 +346,7 @@ def test_run_actions_does_not_raise_for_rcreate_action(): 'global': flexmock(monitoring_verbosity=1, dry_run=False), 'rcreate': flexmock( encryption_mode=flexmock(), - key_repository=flexmock(), + source_repository=flexmock(), copy_crypt_key=flexmock(), append_only=flexmock(), storage_quota=flexmock(), @@ -371,6 +371,30 @@ def test_run_actions_does_not_raise_for_rcreate_action(): ) +def test_run_actions_does_not_raise_for_transfer_action(): + flexmock(module.borg_transfer).should_receive('transfer_archives') + arguments = { + 'global': flexmock(monitoring_verbosity=1, dry_run=False), + 'transfer': flexmock(), + } + + list( + module.run_actions( + arguments=arguments, + config_filename='test.yaml', + location={'repositories': ['repo']}, + storage={}, + retention={}, + consistency={}, + hooks={}, + local_path=None, + remote_path=None, + local_borg_version=None, + repository_path='repo', + ) + ) + + def test_run_actions_calls_hooks_for_prune_action(): flexmock(module.borg_prune).should_receive('prune_archives') flexmock(module.command).should_receive('execute_hook').twice() From f9d7faf88418ed4a1aaafec9bcd21597c722a823 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Thu, 18 Aug 2022 23:33:05 -0700 Subject: [PATCH 35/38] Fix mount action to work without archive again (#557). --- borgmatic/borg/mount.py | 6 ++++- borgmatic/commands/arguments.py | 5 ++-- tests/unit/borg/test_mount.py | 41 +++++++++++++++++++++++---------- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/borgmatic/borg/mount.py b/borgmatic/borg/mount.py index 370ca13ca..c1ad1c3d1 100644 --- a/borgmatic/borg/mount.py +++ b/borgmatic/borg/mount.py @@ -42,7 +42,11 @@ def mount_archive( + ('--glob-archives', archive) ) if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version) - else flags.make_repository_archive_flags(repository, archive, local_borg_version) + else ( + flags.make_repository_archive_flags(repository, archive, local_borg_version) + if archive + else flags.make_repository_flags(repository, local_borg_version) + ) ) + (mount_point,) + (tuple(paths) if paths else ()) diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index bb3ed4b7c..e5be542bb 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -269,8 +269,8 @@ def make_parsers(): transfer_parser = subparsers.add_parser( 'transfer', aliases=SUBPARSER_ALIASES['transfer'], - help='Transfer archives from one repository to another, optionally upgrading the transferred data', - description='Transfer archives from one repository to another, optionally upgrading the transferred data', + help='Transfer archives from one repository to another, optionally upgrading the transferred data (Borg 2.0+ only)', + description='Transfer archives from one repository to another, optionally upgrading the transferred data (Borg 2.0+ only)', add_help=False, ) transfer_group = transfer_parser.add_argument_group('transfer arguments') @@ -290,7 +290,6 @@ def make_parsers(): transfer_group.add_argument( '--upgrader', help='Upgrader type used to convert the transfered data, e.g. "From12To20" to upgrade data from Borg 1.2 to 2.0 format, defaults to no conversion', - required=True, ) transfer_group.add_argument( '-a', diff --git a/tests/unit/borg/test_mount.py b/tests/unit/borg/test_mount.py index d87a18c83..98bbe1a67 100644 --- a/tests/unit/borg/test_mount.py +++ b/tests/unit/borg/test_mount.py @@ -14,16 +14,14 @@ def insert_execute_command_mock(command): ).once() -def test_mount_archive_calls_borg_with_required_parameters(): +def test_mount_archive_calls_borg_with_required_flags(): flexmock(module.feature).should_receive('available').and_return(False) - flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( - ('repo::archive',) - ) - insert_execute_command_mock(('borg', 'mount', 'repo::archive', '/mnt')) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + insert_execute_command_mock(('borg', 'mount', 'repo', '/mnt')) module.mount_archive( repository='repo', - archive='archive', + archive=None, mount_point='/mnt', paths=None, foreground=False, @@ -52,7 +50,26 @@ def test_mount_archive_with_borg_features_calls_borg_with_repository_and_glob_ar ) -def test_mount_archive_calls_borg_with_path_parameters(): +def test_mount_archive_without_archive_calls_borg_with_repository_flags_only(): + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) + insert_execute_command_mock(('borg', 'mount', 'repo::archive', '/mnt')) + + module.mount_archive( + repository='repo', + archive='archive', + mount_point='/mnt', + paths=None, + foreground=False, + options=None, + storage_config={}, + local_borg_version='1.2.3', + ) + + +def test_mount_archive_calls_borg_with_path_flags(): flexmock(module.feature).should_receive('available').and_return(False) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( ('repo::archive',) @@ -71,7 +88,7 @@ def test_mount_archive_calls_borg_with_path_parameters(): ) -def test_mount_archive_calls_borg_with_remote_path_parameters(): +def test_mount_archive_calls_borg_with_remote_path_flags(): flexmock(module.feature).should_receive('available').and_return(False) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( ('repo::archive',) @@ -93,7 +110,7 @@ def test_mount_archive_calls_borg_with_remote_path_parameters(): ) -def test_mount_archive_calls_borg_with_umask_parameters(): +def test_mount_archive_calls_borg_with_umask_flags(): flexmock(module.feature).should_receive('available').and_return(False) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( ('repo::archive',) @@ -112,7 +129,7 @@ def test_mount_archive_calls_borg_with_umask_parameters(): ) -def test_mount_archive_calls_borg_with_lock_wait_parameters(): +def test_mount_archive_calls_borg_with_lock_wait_flags(): flexmock(module.feature).should_receive('available').and_return(False) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( ('repo::archive',) @@ -151,7 +168,7 @@ def test_mount_archive_with_log_info_calls_borg_with_info_parameter(): ) -def test_mount_archive_with_log_debug_calls_borg_with_debug_parameters(): +def test_mount_archive_with_log_debug_calls_borg_with_debug_flags(): flexmock(module.feature).should_receive('available').and_return(False) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( ('repo::archive',) @@ -196,7 +213,7 @@ def test_mount_archive_calls_borg_with_foreground_parameter(): ) -def test_mount_archive_calls_borg_with_options_parameters(): +def test_mount_archive_calls_borg_with_options_flags(): flexmock(module.feature).should_receive('available').and_return(False) flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( ('repo::archive',) From 540f9f6b720d7dfab68c1b95f6eab79e73cb3024 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Fri, 19 Aug 2022 09:40:29 -0700 Subject: [PATCH 36/38] Add missing test for "transfer" action (#557). --- tests/integration/commands/test_arguments.py | 15 ++ tests/unit/borg/test_transfer.py | 267 +++++++++++++++++++ 2 files changed, 282 insertions(+) create mode 100644 tests/unit/borg/test_transfer.py diff --git a/tests/integration/commands/test_arguments.py b/tests/integration/commands/test_arguments.py index eb2a1f9ed..26c569833 100644 --- a/tests/integration/commands/test_arguments.py +++ b/tests/integration/commands/test_arguments.py @@ -501,6 +501,21 @@ def test_parse_arguments_disallows_json_with_both_rinfo_and_info(): module.parse_arguments('rinfo', 'info', '--json') +def test_parse_arguments_disallows_transfer_with_both_archive_and_glob_archives(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(ValueError): + module.parse_arguments( + 'transfer', + '--source-repository', + 'source.borg', + '--archive', + 'foo', + '--glob-archives', + '*bar', + ) + + def test_parse_arguments_disallows_info_with_both_archive_and_glob_archives(): flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) diff --git a/tests/unit/borg/test_transfer.py b/tests/unit/borg/test_transfer.py new file mode 100644 index 000000000..7e605639b --- /dev/null +++ b/tests/unit/borg/test_transfer.py @@ -0,0 +1,267 @@ +import logging + +import pytest +from flexmock import flexmock + +from borgmatic.borg import transfer as module + +from ..test_verbosity import insert_logging_mock + + +def test_transfer_archives_calls_borg_with_flags(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'transfer', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.transfer_archives( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + transfer_arguments=flexmock(archive=None, glob_archives=None, source_repository=None), + ) + + +def test_transfer_archives_with_dry_run_calls_borg_with_dry_run_flag(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args('dry-run', True).and_return( + ('--dry-run',) + ) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'transfer', '--repo', 'repo', '--dry-run'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.transfer_archives( + dry_run=True, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + transfer_arguments=flexmock(archive=None, glob_archives=None, source_repository=None), + ) + + +def test_transfer_archives_with_log_info_calls_borg_with_info_flag(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'transfer', '--info', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + insert_logging_mock(logging.INFO) + module.transfer_archives( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + transfer_arguments=flexmock(archive=None, glob_archives=None, source_repository=None), + ) + + +def test_transfer_archives_with_log_debug_calls_borg_with_debug_flag(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'transfer', '--debug', '--show-rc', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + insert_logging_mock(logging.DEBUG) + + module.transfer_archives( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + transfer_arguments=flexmock(archive=None, glob_archives=None, source_repository=None), + ) + + +def test_transfer_archives_with_archive_calls_borg_with_glob_archives_flag(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args( + 'glob-archives', 'archive' + ).and_return(('--glob-archives', 'archive')) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'transfer', '--glob-archives', 'archive', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.transfer_archives( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + transfer_arguments=flexmock(archive='archive', glob_archives=None, source_repository=None), + ) + + +def test_transfer_archives_with_glob_archives_calls_borg_with_glob_archives_flag(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args( + 'glob-archives', 'foo*' + ).and_return(('--glob-archives', 'foo*')) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'transfer', '--glob-archives', 'foo*', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.transfer_archives( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + transfer_arguments=flexmock(archive=None, glob_archives='foo*', source_repository=None), + ) + + +def test_transfer_archives_with_local_path_calls_borg_via_local_path(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg2', 'transfer', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg2', + extra_environment=None, + ) + + module.transfer_archives( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + transfer_arguments=flexmock(archive=None, glob_archives=None, source_repository=None), + local_path='borg2', + ) + + +def test_transfer_archives_with_remote_path_calls_borg_with_remote_path_flags(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args( + 'remote-path', 'borg2' + ).and_return(('--remote-path', 'borg2')) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'transfer', '--remote-path', 'borg2', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.transfer_archives( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + transfer_arguments=flexmock(archive=None, glob_archives=None, source_repository=None), + remote_path='borg2', + ) + + +def test_transfer_archives_with_lock_wait_calls_borg_with_lock_wait_flags(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args('lock-wait', 5).and_return( + ('--lock-wait', '5') + ) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) + storage_config = {'lock_wait': 5} + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'transfer', '--lock-wait', '5', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.transfer_archives( + dry_run=False, + repository='repo', + storage_config=storage_config, + local_borg_version='2.3.4', + transfer_arguments=flexmock(archive=None, glob_archives=None, source_repository=None), + ) + + +@pytest.mark.parametrize('argument_name', ('upgrader', 'sort_by', 'first', 'last')) +def test_transfer_archives_passes_through_arguments_to_borg(argument_name): + flag_name = f"--{argument_name.replace('_', ' ')}" + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return( + (flag_name, 'value') + ) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'transfer', flag_name, 'value', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.transfer_archives( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + transfer_arguments=flexmock( + archive=None, glob_archives=None, source_repository=None, **{argument_name: 'value'} + ), + ) + + +def test_transfer_archives_with_source_repository_calls_borg_with_other_repo_flags(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args('other-repo', 'other').and_return( + ('--other-repo', 'other') + ) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'transfer', '--repo', 'repo', '--other-repo', 'other'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.transfer_archives( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + transfer_arguments=flexmock(archive=None, glob_archives=None, source_repository='other'), + ) From 8576ac86b909d06d759ff131e2ffa027c3bdc9a8 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Fri, 19 Aug 2022 09:44:31 -0700 Subject: [PATCH 37/38] Fix incorrect version in documentation (#557). --- docs/how-to/set-up-backups.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how-to/set-up-backups.md b/docs/how-to/set-up-backups.md index e1a58ae51..142ea62ef 100644 --- a/docs/how-to/set-up-backups.md +++ b/docs/how-to/set-up-backups.md @@ -197,7 +197,7 @@ command like the following with Borg 1.x: sudo borgmatic init --encryption repokey ``` -New in borgmatic version 2.0.0 +New in borgmatic version 1.7.0 Or, with Borg 2.x: ```bash From 22628ba5d470f221d95a952d339fb010514061c4 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Fri, 19 Aug 2022 12:00:40 -0700 Subject: [PATCH 38/38] Update ssh:// examples in documentation to use relative paths on the remote machine (#557). --- README.md | 4 ++-- .../backup-to-a-removable-drive-or-an-intermittent-server.md | 2 +- docs/how-to/make-backups-redundant.md | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c8ffd7e44..05d4d9bf4 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ location: # Paths of local or remote repositories to backup to. repositories: - - ssh://1234@usw-s001.rsync.net/backups.borg - - ssh://k8pDxu32@k8pDxu32.repo.borgbase.com/repo + - ssh://1234@usw-s001.rsync.net/./backups.borg + - ssh://k8pDxu32@k8pDxu32.repo.borgbase.com/./repo - /var/lib/backups/local.borg retention: diff --git a/docs/how-to/backup-to-a-removable-drive-or-an-intermittent-server.md b/docs/how-to/backup-to-a-removable-drive-or-an-intermittent-server.md index f84fcf49e..689284baa 100644 --- a/docs/how-to/backup-to-a-removable-drive-or-an-intermittent-server.md +++ b/docs/how-to/backup-to-a-removable-drive-or-an-intermittent-server.md @@ -76,7 +76,7 @@ location: - /home repositories: - - ssh://me@buddys-server.org/backup.borg + - ssh://me@buddys-server.org/./backup.borg hooks: before_backup: diff --git a/docs/how-to/make-backups-redundant.md b/docs/how-to/make-backups-redundant.md index 650270b8f..5ad13ee0c 100644 --- a/docs/how-to/make-backups-redundant.md +++ b/docs/how-to/make-backups-redundant.md @@ -20,8 +20,8 @@ location: # Paths of local or remote repositories to backup to. repositories: - - ssh://1234@usw-s001.rsync.net/backups.borg - - ssh://k8pDxu32@k8pDxu32.repo.borgbase.com/repo + - ssh://1234@usw-s001.rsync.net/./backups.borg + - ssh://k8pDxu32@k8pDxu32.repo.borgbase.com/./repo - /var/lib/backups/local.borg ```