diff --git a/NEWS b/NEWS index 0dad11595..4741e71cf 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,7 @@ 1.1.13.dev0 * #54: Fix for incorrect consistency check flags passed to Borg when all three checks ("repository", "archives", and "extract") are specified in borgmatic configuration. + * #48: Add "local_path" to configuration for specifying an alternative Borg executable path. * #49: Support for Borg experimental --patterns-from and --patterns options for specifying mixed includes/excludes. * Moved issue tracker from Taiga to integrated Gitea tracker at diff --git a/borgmatic/borg/check.py b/borgmatic/borg/check.py index 04bac9ce8..41a353c10 100644 --- a/borgmatic/borg/check.py +++ b/borgmatic/borg/check.py @@ -60,10 +60,10 @@ def _make_check_flags(checks, check_last=None): ) + last_flag -def check_archives(verbosity, repository, consistency_config, remote_path=None): +def check_archives(verbosity, repository, consistency_config, local_path='borg', remote_path=None): ''' Given a verbosity flag, a local or remote repository path, a consistency config dict, and a - command to run, check the contained Borg archives for consistency. + local/remote commands to run, check the contained Borg archives for consistency. If there are no consistency checks to run, skip running them. ''' @@ -78,7 +78,7 @@ def check_archives(verbosity, repository, consistency_config, remote_path=None): }.get(verbosity, ()) full_command = ( - 'borg', 'check', + local_path, 'check', repository, ) + _make_check_flags(checks, check_last) + remote_path_flags + verbosity_flags @@ -89,4 +89,4 @@ def check_archives(verbosity, repository, consistency_config, remote_path=None): subprocess.check_call(full_command, stdout=stdout, stderr=subprocess.STDOUT) if 'extract' in checks: - extract.extract_last_archive_dry_run(verbosity, repository, remote_path) + extract.extract_last_archive_dry_run(verbosity, repository, local_path, remote_path) diff --git a/borgmatic/borg/create.py b/borgmatic/borg/create.py index 2285a4564..ea5d4deba 100644 --- a/borgmatic/borg/create.py +++ b/borgmatic/borg/create.py @@ -85,7 +85,7 @@ def _make_exclude_flags(location_config, exclude_filename=None): def create_archive( - verbosity, repository, location_config, storage_config, + verbosity, repository, location_config, storage_config, local_path='borg', remote_path=None, ): ''' Given a vebosity flag, a local or remote repository path, a location config dict, and a storage @@ -117,7 +117,6 @@ def create_archive( one_file_system_flags = ('--one-file-system',) if location_config.get('one_file_system') else () files_cache = location_config.get('files_cache') files_cache_flags = ('--files-cache', files_cache) if files_cache else () - remote_path = location_config.get('remote_path') remote_path_flags = ('--remote-path', remote_path) if remote_path else () verbosity_flags = { VERBOSITY_SOME: ('--info', '--stats',), @@ -127,7 +126,7 @@ def create_archive( archive_name_format = storage_config.get('archive_name_format', default_archive_name_format) full_command = ( - 'borg', 'create', + local_path, 'create', '{repository}::{archive_name_format}'.format( repository=repository, archive_name_format=archive_name_format, diff --git a/borgmatic/borg/extract.py b/borgmatic/borg/extract.py index ee6404091..3d722adc5 100644 --- a/borgmatic/borg/extract.py +++ b/borgmatic/borg/extract.py @@ -8,7 +8,7 @@ from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS logger = logging.getLogger(__name__) -def extract_last_archive_dry_run(verbosity, repository, remote_path=None): +def extract_last_archive_dry_run(verbosity, repository, local_path='borg', remote_path=None): ''' Perform an extraction dry-run of just the most recent archive. If there are no archives, skip the dry-run. @@ -20,7 +20,7 @@ def extract_last_archive_dry_run(verbosity, repository, remote_path=None): }.get(verbosity, ()) full_list_command = ( - 'borg', 'list', + local_path, 'list', '--short', repository, ) + remote_path_flags + verbosity_flags @@ -33,7 +33,7 @@ def extract_last_archive_dry_run(verbosity, repository, remote_path=None): list_flag = ('--list',) if verbosity == VERBOSITY_LOTS else () full_extract_command = ( - 'borg', 'extract', + local_path, 'extract', '--dry-run', '{repository}::{last_archive_name}'.format( repository=repository, diff --git a/borgmatic/borg/prune.py b/borgmatic/borg/prune.py index 918f9840f..83890de94 100644 --- a/borgmatic/borg/prune.py +++ b/borgmatic/borg/prune.py @@ -32,7 +32,7 @@ def _make_prune_flags(retention_config): ) -def prune_archives(verbosity, repository, retention_config, remote_path=None): +def prune_archives(verbosity, repository, retention_config, local_path='borg', remote_path=None): ''' Given a verbosity flag, a local or remote repository path, a retention config dict, prune Borg archives according the the retention policy specified in that configuration. @@ -44,7 +44,7 @@ def prune_archives(verbosity, repository, retention_config, remote_path=None): }.get(verbosity, ()) full_command = ( - 'borg', 'prune', + local_path, 'prune', repository, ) + tuple( element diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index a1ecff764..d1772796c 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -93,6 +93,7 @@ def run_configuration(config_filename, args): # pragma: no cover ) try: + local_path = location.get('local_path', 'borg') remote_path = location.get('remote_path') create.initialize_environment(storage) hook.execute_hook(hooks.get('before_backup'), config_filename, 'pre-backup') @@ -101,7 +102,13 @@ def run_configuration(config_filename, args): # pragma: no cover repository = os.path.expanduser(unexpanded_repository) if args.prune: logger.info('{}: Pruning archives'.format(repository)) - prune.prune_archives(args.verbosity, repository, retention, remote_path=remote_path) + prune.prune_archives( + args.verbosity, + repository, + retention, + local_path=local_path, + remote_path=remote_path, + ) if args.create: logger.info('{}: Creating archive'.format(repository)) create.create_archive( @@ -109,10 +116,18 @@ def run_configuration(config_filename, args): # pragma: no cover repository, location, storage, + local_path=local_path, + remote_path=remote_path, ) if args.check: logger.info('{}: Running consistency checks'.format(repository)) - check.check_archives(args.verbosity, repository, consistency, remote_path=remote_path) + check.check_archives( + args.verbosity, + repository, + consistency, + local_path=local_path, + remote_path=remote_path, + ) hook.execute_hook(hooks.get('after_backup'), config_filename, 'post-backup') except (OSError, CalledProcessError): diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index d05c9b392..de9a8056b 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -29,6 +29,10 @@ map: https://borgbackup.readthedocs.io/en/stable/usage/create.html#description for details. example: ctime,size,inode + local_path: + type: scalar + desc: Alternate Borg local executable. Defaults to "borg". + example: borg1 remote_path: type: scalar desc: Alternate Borg remote executable. Defaults to "borg". diff --git a/borgmatic/tests/unit/borg/test_check.py b/borgmatic/tests/unit/borg/test_check.py index ed9339ccf..52b1b62ad 100644 --- a/borgmatic/tests/unit/borg/test_check.py +++ b/borgmatic/tests/unit/borg/test_check.py @@ -87,7 +87,7 @@ def test_make_check_flags_with_default_checks_and_last_returns_last_flag(): ('repository', 'archives', 'other'), ), ) -def test_check_archives_should_call_borg_with_parameters(checks): +def test_check_archives_calls_borg_with_parameters(checks): check_last = flexmock() consistency_config = flexmock().should_receive('get').and_return(check_last).mock flexmock(module).should_receive('_parse_checks').and_return(checks) @@ -107,7 +107,7 @@ def test_check_archives_should_call_borg_with_parameters(checks): ) -def test_check_archives_with_extract_check_should_call_extract_only(): +def test_check_archives_with_extract_check_calls_extract_only(): checks = ('extract',) check_last = flexmock() consistency_config = flexmock().should_receive('get').and_return(check_last).mock @@ -123,7 +123,7 @@ def test_check_archives_with_extract_check_should_call_extract_only(): ) -def test_check_archives_with_verbosity_some_should_call_borg_with_info_parameter(): +def test_check_archives_with_verbosity_some_calls_borg_with_info_parameter(): checks = ('repository',) consistency_config = flexmock().should_receive('get').and_return(None).mock flexmock(module).should_receive('_parse_checks').and_return(checks) @@ -140,7 +140,7 @@ def test_check_archives_with_verbosity_some_should_call_borg_with_info_parameter ) -def test_check_archives_with_verbosity_lots_should_call_borg_with_debug_parameter(): +def test_check_archives_with_verbosity_lots_calls_borg_with_debug_parameter(): checks = ('repository',) consistency_config = flexmock().should_receive('get').and_return(None).mock flexmock(module).should_receive('_parse_checks').and_return(checks) @@ -157,7 +157,7 @@ def test_check_archives_with_verbosity_lots_should_call_borg_with_debug_paramete ) -def test_check_archives_without_any_checks_should_bail(): +def test_check_archives_without_any_checks_bails(): consistency_config = flexmock().should_receive('get').and_return(None).mock flexmock(module).should_receive('_parse_checks').and_return(()) insert_subprocess_never() @@ -169,7 +169,29 @@ def test_check_archives_without_any_checks_should_bail(): ) -def test_check_archives_with_remote_path_should_call_borg_with_remote_path_parameters(): +def test_check_archives_with_local_path_calls_borg_via_local_path(): + checks = ('repository',) + check_last = flexmock() + consistency_config = flexmock().should_receive('get').and_return(check_last).mock + flexmock(module).should_receive('_parse_checks').and_return(checks) + flexmock(module).should_receive('_make_check_flags').with_args(checks, check_last).and_return(()) + stdout = flexmock() + insert_subprocess_mock( + ('borg1', 'check', 'repo'), + stdout=stdout, stderr=STDOUT, + ) + flexmock(sys.modules['builtins']).should_receive('open').and_return(stdout) + flexmock(module.os).should_receive('devnull') + + module.check_archives( + verbosity=None, + repository='repo', + consistency_config=consistency_config, + local_path='borg1', + ) + + +def test_check_archives_with_remote_path_calls_borg_with_remote_path_parameters(): checks = ('repository',) check_last = flexmock() consistency_config = flexmock().should_receive('get').and_return(check_last).mock diff --git a/borgmatic/tests/unit/borg/test_create.py b/borgmatic/tests/unit/borg/test_create.py index 8bca44c08..c788fda52 100644 --- a/borgmatic/tests/unit/borg/test_create.py +++ b/borgmatic/tests/unit/borg/test_create.py @@ -357,6 +357,26 @@ def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters( ) +def test_create_archive_with_local_path_calls_borg_via_local_path(): + flexmock(module).should_receive('_expand_directory').and_return(['foo']).and_return(['bar']) + flexmock(module).should_receive('_write_pattern_file').and_return(None) + flexmock(module).should_receive('_make_pattern_flags').and_return(()) + flexmock(module).should_receive('_make_exclude_flags').and_return(()) + insert_subprocess_mock(('borg1',) + CREATE_COMMAND[1:]) + + module.create_archive( + verbosity=None, + repository='repo', + location_config={ + 'source_directories': ['foo', 'bar'], + 'repositories': ['repo'], + 'exclude_patterns': None, + }, + storage_config={}, + local_path='borg1', + ) + + def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters(): flexmock(module).should_receive('_expand_directory').and_return(['foo']).and_return(['bar']) flexmock(module).should_receive('_write_pattern_file').and_return(None) @@ -370,10 +390,10 @@ def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters( location_config={ 'source_directories': ['foo', 'bar'], 'repositories': ['repo'], - 'remote_path': 'borg1', 'exclude_patterns': None, }, storage_config={}, + remote_path='borg1', ) diff --git a/borgmatic/tests/unit/borg/test_extract.py b/borgmatic/tests/unit/borg/test_extract.py index 929c3243f..194e33cff 100644 --- a/borgmatic/tests/unit/borg/test_extract.py +++ b/borgmatic/tests/unit/borg/test_extract.py @@ -83,6 +83,23 @@ def test_extract_last_archive_dry_run_with_verbosity_lots_should_call_borg_with_ ) +def test_extract_last_archive_dry_run_should_call_borg_via_local_path(): + flexmock(sys.stdout).encoding = 'utf-8' + insert_subprocess_check_output_mock( + ('borg1', 'list', '--short', 'repo'), + result='archive1\narchive2\n'.encode('utf-8'), + ) + insert_subprocess_mock( + ('borg1', 'extract', '--dry-run', 'repo::archive2'), + ) + + module.extract_last_archive_dry_run( + verbosity=None, + repository='repo', + local_path='borg1', + ) + + def test_extract_last_archive_dry_run_should_call_borg_with_remote_path_parameters(): flexmock(sys.stdout).encoding = 'utf-8' insert_subprocess_check_output_mock( diff --git a/borgmatic/tests/unit/borg/test_prune.py b/borgmatic/tests/unit/borg/test_prune.py index ac941fea8..64c553b95 100644 --- a/borgmatic/tests/unit/borg/test_prune.py +++ b/borgmatic/tests/unit/borg/test_prune.py @@ -18,7 +18,7 @@ BASE_PRUNE_FLAGS = ( ) -def test_make_prune_flags_should_return_flags_from_config_plus_default_prefix(): +def test_make_prune_flags_returns_flags_from_config_plus_default_prefix(): retention_config = OrderedDict( ( ('keep_daily', 1), @@ -55,7 +55,7 @@ PRUNE_COMMAND = ( ) -def test_prune_archives_should_call_borg_with_parameters(): +def test_prune_archives_calls_borg_with_parameters(): retention_config = flexmock() flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS, @@ -69,7 +69,7 @@ def test_prune_archives_should_call_borg_with_parameters(): ) -def test_prune_archives_with_verbosity_some_should_call_borg_with_info_parameter(): +def test_prune_archives_with_verbosity_some_calls_borg_with_info_parameter(): retention_config = flexmock() flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS, @@ -83,7 +83,7 @@ def test_prune_archives_with_verbosity_some_should_call_borg_with_info_parameter ) -def test_prune_archives_with_verbosity_lots_should_call_borg_with_debug_parameter(): +def test_prune_archives_with_verbosity_lots_calls_borg_with_debug_parameter(): retention_config = flexmock() flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS, @@ -97,7 +97,22 @@ def test_prune_archives_with_verbosity_lots_should_call_borg_with_debug_paramete ) -def test_prune_archives_with_remote_path_should_call_borg_with_remote_path_parameters(): +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( + BASE_PRUNE_FLAGS, + ) + insert_subprocess_mock(('borg1',) + PRUNE_COMMAND[1:]) + + module.prune_archives( + verbosity=None, + repository='repo', + retention_config=retention_config, + 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( BASE_PRUNE_FLAGS,