From ed7fe5c6d0eb9fee4eb4830d679acd9dd2371d1d Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Thu, 21 Apr 2022 22:08:25 -0700 Subject: [PATCH] Instead of executing "before" command hooks before all borgmatic actions run (and "after" hooks after), execute these hooks right before/after the corresponding action (#473). --- NEWS | 9 +- borgmatic/commands/borgmatic.py | 232 +++++----- ...reparation-and-cleanup-steps-to-backups.md | 30 +- setup.py | 2 +- tests/unit/commands/test_borgmatic.py | 425 ++++++++++++++---- 5 files changed, 478 insertions(+), 220 deletions(-) diff --git a/NEWS b/NEWS index fabee92..0d1aec9 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,11 @@ -1.5.25.dev0 +1.6.0.dev0 + * #473: Instead of executing "before" command hooks before all borgmatic actions run (and "after" + hooks after), execute these hooks right before/after the corresponding action. E.g., + "before_check" now runs immediately before the "check" action. This better supports running + timing-sensitive tasks like pausing containers. Side effect: before/after command hooks now run + once for each configured repository instead of once per configuration file. Additionally, the + "repositories" interpolated variable has been changed to "repository", containing the path to the + current repository for the hook. * #516: Fix handling of TERM signal to exit borgmatic, not just forward the signal to Borg. * #517: Fix borgmatic exit code (so it's zero) when initial Borg calls fail but later retries succeed. diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index 808ee33..d8d2559 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -65,10 +65,6 @@ def run_configuration(config_filename, config, arguments): using_primary_action = {'prune', 'compact', 'create', 'check'}.intersection(arguments) monitoring_log_level = verbosity_to_log_level(global_arguments.monitoring_verbosity) - hook_context = { - 'repositories': ','.join(location['repositories']), - } - try: local_borg_version = borg_version.local_borg_version(local_path) except (OSError, CalledProcessError, ValueError) as error: @@ -87,50 +83,6 @@ def run_configuration(config_filename, config, arguments): monitoring_log_level, global_arguments.dry_run, ) - if 'prune' in arguments: - command.execute_hook( - hooks.get('before_prune'), - hooks.get('umask'), - config_filename, - 'pre-prune', - global_arguments.dry_run, - **hook_context, - ) - if 'compact' in arguments: - command.execute_hook( - hooks.get('before_compact'), - hooks.get('umask'), - config_filename, - 'pre-compact', - global_arguments.dry_run, - ) - if 'create' in arguments: - command.execute_hook( - hooks.get('before_backup'), - hooks.get('umask'), - config_filename, - 'pre-backup', - global_arguments.dry_run, - **hook_context, - ) - if 'check' in arguments: - command.execute_hook( - hooks.get('before_check'), - hooks.get('umask'), - config_filename, - 'pre-check', - global_arguments.dry_run, - **hook_context, - ) - if 'extract' in arguments: - command.execute_hook( - hooks.get('before_extract'), - hooks.get('umask'), - config_filename, - 'pre-extract', - global_arguments.dry_run, - **hook_context, - ) if using_primary_action: dispatch.call_hooks( 'ping_monitor', @@ -146,7 +98,7 @@ def run_configuration(config_filename, config, arguments): return encountered_error = error - yield from log_error_records('{}: Error running pre hook'.format(config_filename), error) + yield from log_error_records('{}: Error pinging monitor'.format(config_filename), error) if not encountered_error: repo_queue = Queue() @@ -162,6 +114,7 @@ def run_configuration(config_filename, config, arguments): try: yield from run_actions( arguments=arguments, + config_filename=config_filename, location=location, storage=storage, retention=retention, @@ -188,6 +141,9 @@ def run_configuration(config_filename, config, arguments): ) continue + if command.considered_soft_failure(config_filename, error): + return + yield from log_error_records( '{}: Error running actions for repository'.format(repository_path), error ) @@ -196,58 +152,6 @@ def run_configuration(config_filename, config, arguments): if not encountered_error: try: - if 'prune' in arguments: - command.execute_hook( - hooks.get('after_prune'), - hooks.get('umask'), - config_filename, - 'post-prune', - global_arguments.dry_run, - **hook_context, - ) - if 'compact' in arguments: - command.execute_hook( - hooks.get('after_compact'), - hooks.get('umask'), - config_filename, - 'post-compact', - global_arguments.dry_run, - ) - if 'create' in arguments: - dispatch.call_hooks( - 'remove_database_dumps', - hooks, - config_filename, - dump.DATABASE_HOOK_NAMES, - location, - global_arguments.dry_run, - ) - command.execute_hook( - hooks.get('after_backup'), - hooks.get('umask'), - config_filename, - 'post-backup', - global_arguments.dry_run, - **hook_context, - ) - if 'check' in arguments: - command.execute_hook( - hooks.get('after_check'), - hooks.get('umask'), - config_filename, - 'post-check', - global_arguments.dry_run, - **hook_context, - ) - if 'extract' in arguments: - command.execute_hook( - hooks.get('after_extract'), - hooks.get('umask'), - config_filename, - 'post-extract', - global_arguments.dry_run, - **hook_context, - ) if using_primary_action: dispatch.call_hooks( 'ping_monitor', @@ -271,9 +175,7 @@ def run_configuration(config_filename, config, arguments): return encountered_error = error - yield from log_error_records( - '{}: Error running post hook'.format(config_filename), error - ) + yield from log_error_records('{}: Error pinging monitor'.format(config_filename), error) if encountered_error and using_primary_action: try: @@ -316,6 +218,7 @@ def run_configuration(config_filename, config, arguments): def run_actions( *, arguments, + config_filename, location, storage, retention, @@ -325,20 +228,28 @@ def run_actions( remote_path, local_borg_version, repository_path, -): # pragma: no cover +): ''' - Given parsed command-line arguments as an argparse.ArgumentParser instance, several different - configuration dicts, local and remote paths to Borg, a local Borg version string, and a - repository name, run all actions from the command-line arguments on the given repository. + Given parsed command-line arguments as an argparse.ArgumentParser instance, the configuration + filename, several different configuration dicts, local and remote paths to Borg, a local Borg + version string, and a repository name, run all actions from the command-line arguments on the + given repository. Yield JSON output strings from executing any actions that produce JSON. Raise OSError or subprocess.CalledProcessError if an error occurs running a command for an - action. Raise ValueError if the arguments or configuration passed to action are invalid. + action or a hook. Raise ValueError if the arguments or configuration passed to action are + invalid. ''' repository = os.path.expanduser(repository_path) global_arguments = arguments['global'] dry_run_label = ' (dry run; not making any changes)' if global_arguments.dry_run else '' + hook_context = { + 'repository': repository_path, + # Deprecated: For backwards compatibility with borgmatic < 1.6.0. + 'repositories': ','.join(location['repositories']), + } + if 'init' in arguments: logger.info('{}: Initializing repository'.format(repository)) borg_init.initialize_repository( @@ -351,6 +262,14 @@ def run_actions( remote_path=remote_path, ) if 'prune' in arguments: + command.execute_hook( + hooks.get('before_prune'), + hooks.get('umask'), + config_filename, + 'pre-prune', + global_arguments.dry_run, + **hook_context, + ) logger.info('{}: Pruning archives{}'.format(repository, dry_run_label)) borg_prune.prune_archives( global_arguments.dry_run, @@ -362,7 +281,22 @@ def run_actions( stats=arguments['prune'].stats, files=arguments['prune'].files, ) + command.execute_hook( + hooks.get('after_prune'), + hooks.get('umask'), + config_filename, + 'post-prune', + global_arguments.dry_run, + **hook_context, + ) if 'compact' in arguments: + command.execute_hook( + hooks.get('before_compact'), + hooks.get('umask'), + config_filename, + 'pre-compact', + global_arguments.dry_run, + ) if borg_feature.available(borg_feature.Feature.COMPACT, local_borg_version): logger.info('{}: Compacting segments{}'.format(repository, dry_run_label)) borg_compact.compact_segments( @@ -375,11 +309,26 @@ def run_actions( cleanup_commits=arguments['compact'].cleanup_commits, threshold=arguments['compact'].threshold, ) - else: + else: # pragma: nocover logger.info( '{}: Skipping compact (only available/needed in Borg 1.2+)'.format(repository) ) + command.execute_hook( + hooks.get('after_compact'), + hooks.get('umask'), + config_filename, + 'post-compact', + global_arguments.dry_run, + ) if 'create' in arguments: + command.execute_hook( + hooks.get('before_backup'), + hooks.get('umask'), + config_filename, + 'pre-backup', + global_arguments.dry_run, + **hook_context, + ) logger.info('{}: Creating archive{}'.format(repository, dry_run_label)) dispatch.call_hooks( 'remove_database_dumps', @@ -413,10 +362,35 @@ def run_actions( files=arguments['create'].files, stream_processes=stream_processes, ) - if json_output: + if json_output: # pragma: nocover yield json.loads(json_output) + dispatch.call_hooks( + 'remove_database_dumps', + hooks, + config_filename, + dump.DATABASE_HOOK_NAMES, + location, + global_arguments.dry_run, + ) + command.execute_hook( + hooks.get('after_backup'), + hooks.get('umask'), + config_filename, + 'post-backup', + global_arguments.dry_run, + **hook_context, + ) + if 'check' in arguments and checks.repository_enabled_for_checks(repository, consistency): + command.execute_hook( + hooks.get('before_check'), + hooks.get('umask'), + config_filename, + 'pre-check', + global_arguments.dry_run, + **hook_context, + ) logger.info('{}: Running consistency checks'.format(repository)) borg_check.check_archives( repository, @@ -428,7 +402,23 @@ def run_actions( repair=arguments['check'].repair, only_checks=arguments['check'].only, ) + command.execute_hook( + hooks.get('after_check'), + hooks.get('umask'), + config_filename, + 'post-check', + global_arguments.dry_run, + **hook_context, + ) if 'extract' in arguments: + command.execute_hook( + hooks.get('before_extract'), + hooks.get('umask'), + config_filename, + 'pre-extract', + global_arguments.dry_run, + **hook_context, + ) if arguments['extract'].repository is None or validate.repositories_match( repository, arguments['extract'].repository ): @@ -451,6 +441,14 @@ def run_actions( strip_components=arguments['extract'].strip_components, progress=arguments['extract'].progress, ) + command.execute_hook( + hooks.get('after_extract'), + hooks.get('umask'), + config_filename, + 'post-extract', + global_arguments.dry_run, + **hook_context, + ) if 'export-tar' in arguments: if arguments['export-tar'].repository is None or validate.repositories_match( repository, arguments['export-tar'].repository @@ -483,7 +481,7 @@ def run_actions( logger.info( '{}: Mounting archive {}'.format(repository, arguments['mount'].archive) ) - else: + else: # pragma: nocover logger.info('{}: Mounting repository'.format(repository)) borg_mount.mount_archive( @@ -499,7 +497,7 @@ def run_actions( local_path=local_path, remote_path=remote_path, ) - if 'restore' in arguments: + if 'restore' in arguments: # pragma: nocover if arguments['restore'].repository is None or validate.repositories_match( repository, arguments['restore'].repository ): @@ -598,7 +596,7 @@ def run_actions( repository, arguments['list'].repository ): list_arguments = copy.copy(arguments['list']) - if not list_arguments.json: + 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 @@ -610,14 +608,14 @@ def run_actions( local_path=local_path, remote_path=remote_path, ) - if json_output: + 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: + if not info_arguments.json: # pragma: nocover logger.warning('{}: Displaying summary info for archives'.format(repository)) info_arguments.archive = borg_list.resolve_archive_name( repository, info_arguments.archive, storage, local_path, remote_path @@ -629,7 +627,7 @@ def run_actions( local_path=local_path, remote_path=remote_path, ) - if json_output: + if json_output: # pragma: nocover yield json.loads(json_output) if 'borg' in arguments: if arguments['borg'].repository is None or validate.repositories_match( diff --git a/docs/how-to/add-preparation-and-cleanup-steps-to-backups.md b/docs/how-to/add-preparation-and-cleanup-steps-to-backups.md index 85864be..1304793 100644 --- a/docs/how-to/add-preparation-and-cleanup-steps-to-backups.md +++ b/docs/how-to/add-preparation-and-cleanup-steps-to-backups.md @@ -7,11 +7,12 @@ eleventyNavigation: --- ## Preparation and cleanup hooks -If you find yourself performing prepraration tasks before your backup runs, or +If you find yourself performing preparation tasks before your backup runs, or cleanup work afterwards, borgmatic hooks may be of interest. Hooks are shell -commands that borgmatic executes for you at various points, and they're -configured in the `hooks` section of your configuration file. But if you're -looking to backup a database, it's probably easier to use the [database backup +commands that borgmatic executes for you at various points as it runs, and +they're configured in the `hooks` section of your configuration file. But if +you're looking to backup a database, it's probably easier to use the [database +backup feature](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/) instead. @@ -27,15 +28,14 @@ hooks: - umount /some/filesystem ``` -The `before_backup` and `after_backup` hooks each run once per configuration -file. `before_backup` hooks run prior to backups of all repositories in a -configuration file, right before the `create` action. `after_backup` hooks run -afterwards, but not if an error occurs in a previous hook or in the backups -themselves. +The `before_backup` and `after_backup` hooks each run once per repository in a +configuration file. `before_backup` hooks runs right before the `create` +action for a particular repository, and `after_backup` hooks run afterwards, +but not if an error occurs in a previous hook or in the backups themselves. There are additional hooks that run before/after other actions as well. For -instance, `before_prune` runs before a `prune` action, while `after_prune` -runs after it. +instance, `before_prune` runs before a `prune` action for a repository, while +`after_prune` runs after it. ## Variable interpolation @@ -46,18 +46,18 @@ separate shell script: ```yaml hooks: after_prune: - - record-prune.sh "{configuration_filename}" "{repositories}" + - record-prune.sh "{configuration_filename}" "{repository}" ``` In this example, when the hook is triggered, borgmatic interpolates runtime values into the hook command: the borgmatic configuration filename and the -paths of all configured repositories. Here's the full set of supported +paths of the current Borg repository. Here's the full set of supported variables you can use here: * `configuration_filename`: borgmatic configuration filename in which the hook was defined - * `repositories`: comma-separated paths of all repositories configured in the - current borgmatic configuration file + * `repository`: path of the current repository as configured in the current + borgmatic configuration file ## Global hooks diff --git a/setup.py b/setup.py index 81deae9..a588dbb 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import find_packages, setup -VERSION = '1.5.25.dev0' +VERSION = '1.6.0.dev0' setup( diff --git a/tests/unit/commands/test_borgmatic.py b/tests/unit/commands/test_borgmatic.py index 874c8d4..3a2c4f1 100644 --- a/tests/unit/commands/test_borgmatic.py +++ b/tests/unit/commands/test_borgmatic.py @@ -35,75 +35,36 @@ def test_run_configuration_with_invalid_borg_version_errors(): list(module.run_configuration('test.yaml', config, arguments)) -def test_run_configuration_calls_hooks_for_prune_action(): +def test_run_configuration_logs_monitor_start_error(): flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock()) - flexmock(module.command).should_receive('execute_hook').twice() - flexmock(module.dispatch).should_receive('call_hooks').at_least().twice() - flexmock(module).should_receive('run_actions').and_return([]) - config = {'location': {'repositories': ['foo']}} - arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'prune': flexmock()} - - list(module.run_configuration('test.yaml', config, arguments)) - - -def test_run_configuration_calls_hooks_for_compact_action(): - flexmock(module.borg_environment).should_receive('initialize') - flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock()) - flexmock(module.command).should_receive('execute_hook').twice() - flexmock(module).should_receive('run_actions').and_return([]) - config = {'location': {'repositories': ['foo']}} - arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'compact': flexmock()} - - list(module.run_configuration('test.yaml', config, arguments)) - - -def test_run_configuration_executes_and_calls_hooks_for_create_action(): - flexmock(module.borg_environment).should_receive('initialize') - flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock()) - flexmock(module.command).should_receive('execute_hook').twice() - flexmock(module.dispatch).should_receive('call_hooks').at_least().twice() - flexmock(module).should_receive('run_actions').and_return([]) + flexmock(module.dispatch).should_receive('call_hooks').and_raise(OSError).and_return( + None + ).and_return(None) + expected_results = [flexmock()] + flexmock(module).should_receive('log_error_records').and_return(expected_results) + flexmock(module).should_receive('run_actions').never() config = {'location': {'repositories': ['foo']}} arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()} - list(module.run_configuration('test.yaml', config, arguments)) + results = list(module.run_configuration('test.yaml', config, arguments)) + + assert results == expected_results -def test_run_configuration_calls_hooks_for_check_action(): +def test_run_configuration_bails_for_monitor_start_soft_failure(): flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock()) - flexmock(module.command).should_receive('execute_hook').twice() - flexmock(module.dispatch).should_receive('call_hooks').at_least().twice() - flexmock(module).should_receive('run_actions').and_return([]) + error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again') + flexmock(module.dispatch).should_receive('call_hooks').and_raise(error) + flexmock(module).should_receive('log_error_records').never() + flexmock(module).should_receive('run_actions').never() config = {'location': {'repositories': ['foo']}} - arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'check': flexmock()} + arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()} - list(module.run_configuration('test.yaml', config, arguments)) + results = list(module.run_configuration('test.yaml', config, arguments)) - -def test_run_configuration_calls_hooks_for_extract_action(): - flexmock(module.borg_environment).should_receive('initialize') - flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock()) - flexmock(module.command).should_receive('execute_hook').twice() - flexmock(module.dispatch).should_receive('call_hooks').never() - flexmock(module).should_receive('run_actions').and_return([]) - config = {'location': {'repositories': ['foo']}} - arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'extract': flexmock()} - - list(module.run_configuration('test.yaml', config, arguments)) - - -def test_run_configuration_does_not_trigger_hooks_for_list_action(): - flexmock(module.borg_environment).should_receive('initialize') - flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock()) - flexmock(module.command).should_receive('execute_hook').never() - flexmock(module.dispatch).should_receive('call_hooks').never() - flexmock(module).should_receive('run_actions').and_return([]) - config = {'location': {'repositories': ['foo']}} - arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'list': flexmock()} - - list(module.run_configuration('test.yaml', config, arguments)) + assert results == [] def test_run_configuration_logs_actions_error(): @@ -122,28 +83,14 @@ def test_run_configuration_logs_actions_error(): assert results == expected_results -def test_run_configuration_logs_pre_hook_error(): - flexmock(module.borg_environment).should_receive('initialize') - flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock()) - flexmock(module.command).should_receive('execute_hook').and_raise(OSError).and_return(None) - expected_results = [flexmock()] - flexmock(module).should_receive('log_error_records').and_return(expected_results) - flexmock(module).should_receive('run_actions').never() - config = {'location': {'repositories': ['foo']}} - arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()} - - results = list(module.run_configuration('test.yaml', config, arguments)) - - assert results == expected_results - - -def test_run_configuration_bails_for_pre_hook_soft_failure(): +def test_run_configuration_bails_for_actions_soft_failure(): flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock()) + flexmock(module.dispatch).should_receive('call_hooks') error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again') - flexmock(module.command).should_receive('execute_hook').and_raise(error).and_return(None) + flexmock(module).should_receive('run_actions').and_raise(error) flexmock(module).should_receive('log_error_records').never() - flexmock(module).should_receive('run_actions').never() + flexmock(module.command).should_receive('considered_soft_failure').and_return(True) config = {'location': {'repositories': ['foo']}} arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()} @@ -152,13 +99,12 @@ def test_run_configuration_bails_for_pre_hook_soft_failure(): assert results == [] -def test_run_configuration_logs_post_hook_error(): +def test_run_configuration_logs_monitor_finish_error(): flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock()) - flexmock(module.command).should_receive('execute_hook').and_return(None).and_raise( - OSError - ).and_return(None) - flexmock(module.dispatch).should_receive('call_hooks') + flexmock(module.dispatch).should_receive('call_hooks').and_return(None).and_return( + None + ).and_raise(OSError) expected_results = [flexmock()] flexmock(module).should_receive('log_error_records').and_return(expected_results) flexmock(module).should_receive('run_actions').and_return([]) @@ -170,16 +116,16 @@ def test_run_configuration_logs_post_hook_error(): assert results == expected_results -def test_run_configuration_bails_for_post_hook_soft_failure(): +def test_run_configuration_bails_for_monitor_finish_soft_failure(): flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock()) error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again') - flexmock(module.command).should_receive('execute_hook').and_return(None).and_raise( - error - ).and_return(None) - flexmock(module.dispatch).should_receive('call_hooks') + flexmock(module.dispatch).should_receive('call_hooks').and_return(None).and_return( + None + ).and_raise(error) flexmock(module).should_receive('log_error_records').never() flexmock(module).should_receive('run_actions').and_return([]) + flexmock(module.command).should_receive('considered_soft_failure').and_return(True) config = {'location': {'repositories': ['foo']}} arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()} @@ -209,7 +155,7 @@ def test_run_configuration_bails_for_on_error_hook_soft_failure(): flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock()) error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again') - flexmock(module.command).should_receive('execute_hook').and_return(None).and_raise(error) + flexmock(module.command).should_receive('execute_hook').and_raise(error) expected_results = [flexmock()] flexmock(module).should_receive('log_error_records').and_return(expected_results) flexmock(module).should_receive('run_actions').and_raise(OSError) @@ -411,6 +357,313 @@ 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') + arguments = { + 'global': flexmock(monitoring_verbosity=1, dry_run=False), + 'init': flexmock( + encryption_mode=flexmock(), append_only=flexmock(), storage_quota=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() + arguments = { + 'global': flexmock(monitoring_verbosity=1, dry_run=False), + 'prune': flexmock(stats=flexmock(), files=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_compact_action(): + flexmock(module.borg_feature).should_receive('available').and_return(True) + flexmock(module.borg_compact).should_receive('compact_segments') + flexmock(module.command).should_receive('execute_hook').twice() + arguments = { + 'global': flexmock(monitoring_verbosity=1, dry_run=False), + 'compact': flexmock(progress=flexmock(), cleanup_commits=flexmock(), threshold=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_executes_and_calls_hooks_for_create_action(): + flexmock(module.borg_create).should_receive('create_archive') + flexmock(module.command).should_receive('execute_hook').twice() + flexmock(module.dispatch).should_receive('call_hooks').and_return({}).times(3) + arguments = { + 'global': flexmock(monitoring_verbosity=1, dry_run=False), + 'create': flexmock( + progress=flexmock(), stats=flexmock(), json=flexmock(), files=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_check_action(): + flexmock(module.checks).should_receive('repository_enabled_for_checks').and_return(True) + flexmock(module.borg_check).should_receive('check_archives') + flexmock(module.command).should_receive('execute_hook').twice() + arguments = { + 'global': flexmock(monitoring_verbosity=1, dry_run=False), + 'check': flexmock(progress=flexmock(), repair=flexmock(), only=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_extract_action(): + flexmock(module.validate).should_receive('repositories_match').and_return(True) + flexmock(module.borg_extract).should_receive('extract_archive') + flexmock(module.command).should_receive('execute_hook').twice() + arguments = { + 'global': flexmock(monitoring_verbosity=1, dry_run=False), + 'extract': flexmock( + paths=flexmock(), + progress=flexmock(), + destination=flexmock(), + strip_components=flexmock(), + archive=flexmock(), + repository='repo', + ), + } + + 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_export_tar_action(): + flexmock(module.validate).should_receive('repositories_match').and_return(True) + flexmock(module.borg_export_tar).should_receive('export_tar_archive') + arguments = { + 'global': flexmock(monitoring_verbosity=1, dry_run=False), + 'export-tar': flexmock( + repository=flexmock(), + archive=flexmock(), + paths=flexmock(), + destination=flexmock(), + tar_filter=flexmock(), + files=flexmock(), + strip_components=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_mount_action(): + flexmock(module.validate).should_receive('repositories_match').and_return(True) + flexmock(module.borg_mount).should_receive('mount_archive') + arguments = { + 'global': flexmock(monitoring_verbosity=1, dry_run=False), + 'mount': flexmock( + repository=flexmock(), + archive=flexmock(), + mount_point=flexmock(), + paths=flexmock(), + foreground=flexmock(), + options=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') + arguments = { + 'global': flexmock(monitoring_verbosity=1, dry_run=False), + 'list': flexmock(repository=flexmock(), archive=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()) + flexmock(module.borg_info).should_receive('display_archives_info') + arguments = { + 'global': flexmock(monitoring_verbosity=1, dry_run=False), + 'info': flexmock(repository=flexmock(), archive=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_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_borg).should_receive('run_arbitrary_borg') + arguments = { + 'global': flexmock(monitoring_verbosity=1, dry_run=False), + 'borg': flexmock(repository=flexmock(), archive=flexmock(), options=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_load_configurations_collects_parsed_configurations(): configuration = flexmock() other_configuration = flexmock()