From 30b52e55232522e5387b603f655a0a03775ce611 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Mon, 24 Dec 2018 22:28:02 -0800 Subject: [PATCH] With --init command-line flag, if a repository already exists, proceed without erroring (#117). --- NEWS | 2 + README.md | 4 ++ borgmatic/borg/init.py | 16 +++++-- borgmatic/commands/borgmatic.py | 6 +-- tests/integration/commands/test_borgmatic.py | 14 +++--- tests/unit/borg/test_init.py | 45 ++++++++++++++------ 6 files changed, 60 insertions(+), 27 deletions(-) diff --git a/NEWS b/NEWS index f7c84f471..c24b3298c 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,7 @@ 1.2.13.dev0 * #100: Support for --stats command-line flag independent of --verbosity. + * #117: With borgmatic --init command-line flag, if a repository already exists, proceed without + erroring. 1.2.12 * #110: Support for Borg repository initialization via borgmatic --init command-line flag. diff --git a/README.md b/README.md index 1e251ead8..3cad8f836 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,10 @@ Also, optionally check out the [Borg Quick Start](https://borgbackup.readthedocs.org/en/latest/quickstart.html) for more background about repository initialization. +Note that borgmatic skips repository initialization if the repository already +exists. This supports use cases like ensuring a repository exists prior to +performing a backup. + If the repository is on a remote host, make sure that your local user has key-based SSH access to the desired user account on the remote host. diff --git a/borgmatic/borg/init.py b/borgmatic/borg/init.py index b9bdf9a87..9ce419846 100644 --- a/borgmatic/borg/init.py +++ b/borgmatic/borg/init.py @@ -15,9 +15,17 @@ def initialize_repository( ): ''' Given a local or remote repository path, a Borg encryption mode, whether the repository should - be append-only, and the storage quota to use, initialize the repository. + be append-only, and the storage quota to use, initialize the repository. If the repository + already exists, then log and skip initialization. ''' - full_command = ( + info_command = (local_path, 'info', repository) + logger.debug(' '.join(info_command)) + + if subprocess.call(info_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0: + logger.info('Repository already exists. Skipping initialization.') + return + + init_command = ( (local_path, 'init', repository) + (('--encryption', encryption_mode) if encryption_mode else ()) + (('--append-only',) if append_only else ()) @@ -27,5 +35,5 @@ def initialize_repository( + (('--remote-path', remote_path) if remote_path else ()) ) - logger.debug(' '.join(full_command)) - subprocess.check_call(full_command) + logger.debug(' '.join(init_command)) + subprocess.check_call(init_command) diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index c543bcfa6..69b9ef093 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -149,10 +149,8 @@ def parse_arguments(*arguments): 'The --encryption, --append-only, and --storage-quota options can only be used with the --init option' ) - if args.init and (args.prune or args.create or args.dry_run): - raise ValueError( - 'The --init option cannot be used with the --prune, --create, or --dry-run options' - ) + if args.init and args.dry_run: + raise ValueError('The --init option cannot be used with the --dry-run option') if args.init and not args.encryption_mode: raise ValueError('The --encryption option is required with the --init option') diff --git a/tests/integration/commands/test_borgmatic.py b/tests/integration/commands/test_borgmatic.py index 82058aa1f..d50a30dcd 100644 --- a/tests/integration/commands/test_borgmatic.py +++ b/tests/integration/commands/test_borgmatic.py @@ -113,25 +113,25 @@ def test_parse_arguments_disallows_storage_quota_without_init(): module.parse_arguments('--config', 'myconfig', '--storage-quota', '5G') -def test_parse_arguments_disallows_init_and_prune(): +def test_parse_arguments_allows_init_and_prune(): flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - with pytest.raises(ValueError): - module.parse_arguments('--config', 'myconfig', '--init', '--prune') + module.parse_arguments('--config', 'myconfig', '--init', '--encryption', 'repokey', '--prune') -def test_parse_arguments_disallows_init_and_create(): +def test_parse_arguments_allows_init_and_create(): flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - with pytest.raises(ValueError): - module.parse_arguments('--config', 'myconfig', '--init', '--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', '--dry-run') + module.parse_arguments( + '--config', 'myconfig', '--init', '--encryption', 'repokey', '--dry-run' + ) def test_parse_arguments_allows_progress_and_create(): diff --git a/tests/unit/borg/test_init.py b/tests/unit/borg/test_init.py index 67ed8a878..d420d862d 100644 --- a/tests/unit/borg/test_init.py +++ b/tests/unit/borg/test_init.py @@ -6,53 +6,74 @@ from borgmatic.borg import init as module from ..test_verbosity import insert_logging_mock -def insert_subprocess_mock(check_call_command, **kwargs): - subprocess = flexmock(module.subprocess) - subprocess.should_receive('check_call').with_args(check_call_command, **kwargs).once() - - +INFO_REPOSITORY_EXISTS_RESPONSE_CODE = 0 +INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE = 2 INIT_COMMAND = ('borg', 'init', 'repo', '--encryption', 'repokey') +def insert_info_command_mock(info_response): + subprocess = flexmock(module.subprocess) + subprocess.should_receive('call').and_return(info_response) + + +def insert_init_command_mock(init_command, **kwargs): + subprocess = flexmock(module.subprocess) + subprocess.should_receive('check_call').with_args(init_command, **kwargs).once() + + def test_initialize_repository_calls_borg_with_parameters(): - insert_subprocess_mock(INIT_COMMAND) + insert_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE) + insert_init_command_mock(INIT_COMMAND) + + module.initialize_repository(repository='repo', encryption_mode='repokey') + + +def test_initialize_repository_skips_initialization_when_repository_already_exists(): + insert_info_command_mock(INFO_REPOSITORY_EXISTS_RESPONSE_CODE) + flexmock(module.subprocess).should_receive('check_call').never() module.initialize_repository(repository='repo', encryption_mode='repokey') def test_initialize_repository_with_append_only_calls_borg_with_append_only_parameter(): - insert_subprocess_mock(INIT_COMMAND + ('--append-only',)) + insert_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE) + insert_init_command_mock(INIT_COMMAND + ('--append-only',)) module.initialize_repository(repository='repo', encryption_mode='repokey', append_only=True) def test_initialize_repository_with_storage_quota_calls_borg_with_storage_quota_parameter(): - insert_subprocess_mock(INIT_COMMAND + ('--storage-quota', '5G')) + insert_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE) + insert_init_command_mock(INIT_COMMAND + ('--storage-quota', '5G')) module.initialize_repository(repository='repo', encryption_mode='repokey', storage_quota='5G') def test_initialize_repository_with_log_info_calls_borg_with_info_parameter(): - insert_subprocess_mock(INIT_COMMAND + ('--info',)) + insert_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE) + insert_init_command_mock(INIT_COMMAND + ('--info',)) insert_logging_mock(logging.INFO) module.initialize_repository(repository='repo', encryption_mode='repokey') def test_initialize_repository_with_log_debug_calls_borg_with_debug_parameter(): - insert_subprocess_mock(INIT_COMMAND + ('--debug',)) + insert_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE) + insert_init_command_mock(INIT_COMMAND + ('--debug',)) insert_logging_mock(logging.DEBUG) module.initialize_repository(repository='repo', encryption_mode='repokey') def test_initialize_repository_with_local_path_calls_borg_via_local_path(): - insert_subprocess_mock(('borg1',) + INIT_COMMAND[1:]) + insert_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE) + insert_init_command_mock(('borg1',) + INIT_COMMAND[1:]) module.initialize_repository(repository='repo', encryption_mode='repokey', local_path='borg1') def test_initialize_repository_with_remote_path_calls_borg_with_remote_path_parameter(): - insert_subprocess_mock(INIT_COMMAND + ('--remote-path', 'borg1')) + insert_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE) + insert_init_command_mock(INIT_COMMAND + ('--remote-path', 'borg1')) module.initialize_repository(repository='repo', encryption_mode='repokey', remote_path='borg1')