diff --git a/NEWS b/NEWS index fa2e0d630..ac7b42047 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,7 @@ 1.5.23.dev0 - * #394: Compact repository segments with new "borgmatic compact" action. Borg 1.2+ only. + * #394: Compact repository segments and free space with new "borgmatic compact" action. Borg 1.2+ + only. Also run "compact" by default when no actions are specified, as "prune" in Borg 1.2 no + longer frees up space unless "compact" is run. * #480, #482: Fix traceback when a YAML validation error occurs. 1.5.22 diff --git a/borgmatic/borg/compact.py b/borgmatic/borg/compact.py index 115fb939c..2df6774f4 100644 --- a/borgmatic/borg/compact.py +++ b/borgmatic/borg/compact.py @@ -38,4 +38,4 @@ def compact_segments( + (repository,) ) - execute_command(full_command, output_log_level=logging.WARNING, borg_local_path=local_path) + execute_command(full_command, output_log_level=logging.INFO, borg_local_path=local_path) diff --git a/borgmatic/borg/feature.py b/borgmatic/borg/feature.py new file mode 100644 index 000000000..970d04c94 --- /dev/null +++ b/borgmatic/borg/feature.py @@ -0,0 +1,20 @@ +from enum import Enum + +from pkg_resources import parse_version + + +class Feature(Enum): + COMPACT = 1 + + +FEATURE_TO_MINIMUM_BORG_VERSION = { + Feature.COMPACT: parse_version('1.2.0a2'), +} + + +def available(feature, borg_version): + ''' + Given a Borg Feature constant and a Borg version string, return whether that feature is + available in that version of Borg. + ''' + return FEATURE_TO_MINIMUM_BORG_VERSION[feature] <= parse_version(borg_version) diff --git a/borgmatic/borg/version.py b/borgmatic/borg/version.py new file mode 100644 index 000000000..4ccab9b5e --- /dev/null +++ b/borgmatic/borg/version.py @@ -0,0 +1,25 @@ +import logging + +from borgmatic.execute import execute_command + +logger = logging.getLogger(__name__) + + +def local_borg_version(local_path='borg'): + ''' + Given a local Borg binary path, return a version string for it. + + Raise OSError or CalledProcessError if there is a problem running Borg. + Raise ValueError if the version cannot be parsed. + ''' + full_command = ( + (local_path, '--version') + + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ()) + + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ()) + ) + output = execute_command(full_command, output_log_level=None, borg_local_path=local_path) + + try: + return output.split(' ')[1].strip() + except IndexError: + raise ValueError('Could not parse Borg version string') diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index 53537dc81..143a963b8 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -63,9 +63,9 @@ def parse_subparser_arguments(unparsed_arguments, subparsers): arguments[canonical_name] = parsed - # If no actions are explicitly requested, assume defaults: prune, create, and check. + # If no actions are explicitly requested, assume defaults: prune, compact, create, and check. if not arguments and '--help' not in unparsed_arguments and '-h' not in unparsed_arguments: - for subparser_name in ('prune', 'create', 'check'): + for subparser_name in ('prune', 'compact', 'create', 'check'): subparser = subparsers[subparser_name] parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments) arguments[subparser_name] = parsed @@ -200,8 +200,8 @@ def parse_arguments(*unparsed_arguments): top_level_parser = ArgumentParser( description=''' Simple, configuration-driven backup software for servers and workstations. If none of - the action options are given, then borgmatic defaults to: prune, create, and check - archives. + the action options are given, then borgmatic defaults to: prune, compact, create, and + check. ''', parents=[global_parser], ) @@ -209,7 +209,7 @@ def parse_arguments(*unparsed_arguments): subparsers = top_level_parser.add_subparsers( title='actions', metavar='', - help='Specify zero or more actions. Defaults to prune, create, and check. Use --help with action for details:', + 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', @@ -242,8 +242,8 @@ def parse_arguments(*unparsed_arguments): prune_parser = subparsers.add_parser( 'prune', aliases=SUBPARSER_ALIASES['prune'], - help='Prune archives according to the retention policy', - description='Prune archives according to the retention policy', + help='Prune archives according to the retention policy (with Borg 1.2+, run compact afterwards to actually free space)', + description='Prune archives according to the retention policy (with Borg 1.2+, run compact afterwards to actually free space)', add_help=False, ) prune_group = prune_parser.add_argument_group('prune arguments') diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index ee92a5a7b..61fc4e296 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -18,12 +18,14 @@ from borgmatic.borg import create as borg_create from borgmatic.borg import environment as borg_environment 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 umount as borg_umount +from borgmatic.borg import version as borg_version from borgmatic.commands.arguments import parse_arguments from borgmatic.config import checks, collect, convert, validate from borgmatic.hooks import command, dispatch, dump, monitor @@ -39,8 +41,8 @@ LEGACY_CONFIG_PATH = '/etc/borgmatic/config' def run_configuration(config_filename, config, arguments): ''' Given a config filename, the corresponding parsed config dict, and command-line arguments as a - dict from subparser name to a namespace of parsed arguments, execute its defined pruning, - backups, consistency checks, and/or other actions. + dict from subparser name to a namespace of parsed arguments, execute the defined prune, compact, + create, check, and/or other actions. Yield a combination of: @@ -60,11 +62,19 @@ def run_configuration(config_filename, config, arguments): borg_environment.initialize(storage) encountered_error = None error_repository = '' - prune_create_or_check = {'prune', 'create', 'check'}.intersection(arguments) + using_primary_action = {'prune', 'compact', 'create', 'check'}.intersection(arguments) monitoring_log_level = verbosity_to_log_level(global_arguments.monitoring_verbosity) try: - if prune_create_or_check: + local_borg_version = borg_version.local_borg_version(local_path) + except (OSError, CalledProcessError, ValueError) as error: + yield from make_error_log_records( + '{}: Error getting local Borg version'.format(config_filename), error + ) + return + + try: + if using_primary_action: dispatch.call_hooks( 'initialize_monitor', hooks, @@ -113,7 +123,7 @@ def run_configuration(config_filename, config, arguments): 'pre-extract', global_arguments.dry_run, ) - if prune_create_or_check: + if using_primary_action: dispatch.call_hooks( 'ping_monitor', hooks, @@ -153,6 +163,7 @@ def run_configuration(config_filename, config, arguments): hooks=hooks, local_path=local_path, remote_path=remote_path, + local_borg_version=local_borg_version, repository_path=repository_path, ) except (OSError, CalledProcessError, ValueError) as error: @@ -218,7 +229,7 @@ def run_configuration(config_filename, config, arguments): 'post-extract', global_arguments.dry_run, ) - if prune_create_or_check: + if using_primary_action: dispatch.call_hooks( 'ping_monitor', hooks, @@ -245,7 +256,7 @@ def run_configuration(config_filename, config, arguments): '{}: Error running post hook'.format(config_filename), error ) - if encountered_error and prune_create_or_check: + if encountered_error and using_primary_action: try: command.execute_hook( hooks.get('on_error'), @@ -293,12 +304,13 @@ def run_actions( hooks, local_path, 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, and a repository name, run all actions - from the command-line arguments on the given repository. + 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. @@ -332,17 +344,22 @@ def run_actions( files=arguments['prune'].files, ) if 'compact' in arguments: - logger.info('{}: Compacting segments{}'.format(repository, dry_run_label)) - borg_compact.compact_segments( - global_arguments.dry_run, - repository, - storage, - local_path=local_path, - remote_path=remote_path, - progress=arguments['compact'].progress, - cleanup_commits=arguments['compact'].cleanup_commits, - threshold=arguments['compact'].threshold, - ) + if borg_feature.available(borg_feature.Feature.COMPACT, local_borg_version): + logger.info('{}: Compacting segments{}'.format(repository, dry_run_label)) + borg_compact.compact_segments( + global_arguments.dry_run, + repository, + storage, + local_path=local_path, + remote_path=remote_path, + progress=arguments['compact'].progress, + cleanup_commits=arguments['compact'].cleanup_commits, + threshold=arguments['compact'].threshold, + ) + else: + logger.info( + '{}: Skipping compact (only available/needed in Borg 1.2+)'.format(repository) + ) if 'create' in arguments: logger.info('{}: Creating archive{}'.format(repository, dry_run_label)) dispatch.call_hooks( diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index f637325d1..18811fb1c 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -605,10 +605,11 @@ properties: type: string description: | List of one or more shell commands or scripts to execute - when an exception occurs during a "prune", "create", or - "check" action or an associated before/after hook. + when an exception occurs during a "prune", "compact", + "create", or "check" action or an associated before/after + hook. example: - - echo "Error during prune/create/check." + - echo "Error during prune/compact/create/check." before_everything: type: array items: 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 0e10e76ea..45bbfbe2c 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 @@ -33,9 +33,9 @@ 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. -There are additional hooks for the `prune` and `check` actions as well. -`before_prune` and `after_prune` run if there are any `prune` actions, while -`before_check` and `after_check` run if there are any `check` actions. +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. You can also use `before_everything` and `after_everything` hooks to perform global setup or cleanup: 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 0c4f83f88..bb88d5083 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 @@ -115,6 +115,6 @@ There are some caveats you should be aware of with this feature. * The soft failure doesn't have to apply to a repository. You can even perform a test to make sure that individual source directories are mounted and available. Use your imagination! - * The soft failure feature also works for `before_prune`, `after_prune`, - `before_check`, and `after_check` hooks. But it is not implemented for - `before_everything` or `after_everything`. + * The soft failure feature also works for before/after hooks for other + actions as well. But it is not implemented for `before_everything` or + `after_everything`. diff --git a/docs/how-to/deal-with-very-large-backups.md b/docs/how-to/deal-with-very-large-backups.md index d1ebc6980..4e810a241 100644 --- a/docs/how-to/deal-with-very-large-backups.md +++ b/docs/how-to/deal-with-very-large-backups.md @@ -9,19 +9,20 @@ eleventyNavigation: Borg itself is great for efficiently de-duplicating data across successive backup archives, even when dealing with very large repositories. But you may -find that while borgmatic's default mode of "prune, create, and check" works -well on small repositories, it's not so great on larger ones. That's because -running the default pruning and consistency checks take a long time on large -repositories. +find that while borgmatic's default mode of `prune`, `compact`, `create`, and +`check` works well on small repositories, it's not so great on larger ones. +That's because running the default pruning, compact, and consistency checks +take a long time on large repositories. ### A la carte actions If you find yourself in this situation, you have some options. First, you can -run borgmatic's pruning, creating, or checking actions separately. For -instance, the following optional actions are available: +run borgmatic's `prune`, `compact`, `create`, or `check` actions separately. +For instance, the following optional actions are available: ```bash borgmatic prune +borgmatic compact borgmatic create borgmatic check ``` @@ -32,7 +33,7 @@ borgmatic check 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 -`prune` and only runs `create` and `check`: +`prune` and `compact` and only runs `create` and `check`: ```bash borgmatic create check diff --git a/docs/how-to/monitor-your-backups.md b/docs/how-to/monitor-your-backups.md index d63a9b73f..e616a29ec 100644 --- a/docs/how-to/monitor-your-backups.md +++ b/docs/how-to/monitor-your-backups.md @@ -83,10 +83,10 @@ tests](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/). ## Error hooks -When an error occurs during a `prune`, `create`, or `check` action, borgmatic -can run configurable shell commands to fire off custom error notifications or -take other actions, so you can get alerted as soon as something goes wrong. -Here's a not-so-useful example: +When an error occurs during a `prune`, `compact`, `create`, or `check` action, +borgmatic can run configurable shell commands to fire off custom error +notifications or take other actions, so you can get alerted as soon as +something goes wrong. Here's a not-so-useful example: ```yaml hooks: @@ -117,9 +117,9 @@ here: * `output`: output of the command that failed (may be blank if an error occurred without running a command) -Note that borgmatic runs the `on_error` hooks only for `prune`, `create`, or -`check` actions or hooks in which an error occurs, and not other actions. -borgmatic does not run `on_error` hooks if an error occurs within a +Note that borgmatic runs the `on_error` hooks only for `prune`, `compact`, +`create`, or `check` actions or hooks in which an error occurs, and not other +actions. borgmatic does not run `on_error` hooks if an error occurs within a `before_everything` or `after_everything` hook. For more about hooks, see the [borgmatic hooks documentation](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/), @@ -144,7 +144,7 @@ With this hook in place, borgmatic pings your Healthchecks project when a backup begins, ends, or errors. Specifically, after the `before_backup` hooks run, borgmatic lets Healthchecks know that it has started if any of -the `prune`, `create`, or `check` actions are run. +the `prune`, `compact`, `create`, or `check` actions are run. Then, if the actions complete successfully, borgmatic notifies Healthchecks of the success after the `after_backup` hooks run, and includes borgmatic logs in @@ -155,7 +155,7 @@ in the Healthchecks UI, although be aware that Healthchecks currently has a If an error occurs during any action or hook, borgmatic notifies Healthchecks after the `on_error` hooks run, also tacking on logs including the error itself. But the logs are only included for errors that occur when a `prune`, -`create`, or `check` action is run. +`compact`, `create`, or `check` action is run. You can customize the verbosity of the logs that are sent to Healthchecks with borgmatic's `--monitoring-verbosity` flag. The `--files` and `--stats` flags @@ -184,8 +184,8 @@ With this hook in place, borgmatic pings your Cronitor monitor when a backup begins, ends, or errors. Specifically, after the `before_backup` hooks run, borgmatic lets Cronitor know that it has started if any of the -`prune`, `create`, or `check` actions are run. Then, if the actions complete -successfully, borgmatic notifies Cronitor of the success after the +`prune`, `compact`, `create`, or `check` actions are run. Then, if the actions +complete successfully, borgmatic notifies Cronitor of the success after the `after_backup` hooks run. And if an error occurs during any action or hook, borgmatic notifies Cronitor after the `on_error` hooks run. @@ -212,8 +212,8 @@ With this hook in place, borgmatic pings your Cronhub monitor when a backup begins, ends, or errors. Specifically, after the `before_backup` hooks run, borgmatic lets Cronhub know that it has started if any of the -`prune`, `create`, or `check` actions are run. Then, if the actions complete -successfully, borgmatic notifies Cronhub of the success after the +`prune`, `compact`, `create`, or `check` actions are run. Then, if the actions +complete successfully, borgmatic notifies Cronhub of the success after the `after_backup` hooks run. And if an error occurs during any action or hook, borgmatic notifies Cronhub after the `on_error` hooks run. @@ -252,9 +252,9 @@ hooks: With this hook in place, borgmatic creates a PagerDuty event for your service whenever backups fail. Specifically, if an error occurs during a `create`, -`prune`, or `check` action, borgmatic sends an event to PagerDuty before the -`on_error` hooks run. Note that borgmatic does not contact PagerDuty when a -backup starts or ends without error. +`prune`, `compact`, or `check` action, borgmatic sends an event to PagerDuty +before the `on_error` hooks run. Note that borgmatic does not contact +PagerDuty when a backup starts or ends without error. You can configure PagerDuty to notify you by a [variety of mechanisms](https://support.pagerduty.com/docs/notifications) when backups diff --git a/docs/how-to/set-up-backups.md b/docs/how-to/set-up-backups.md index 09576419a..333e5ae51 100644 --- a/docs/how-to/set-up-backups.md +++ b/docs/how-to/set-up-backups.md @@ -227,8 +227,8 @@ sudo borgmatic --verbosity 1 --files borgmatic. So try leaving it out, or upgrade borgmatic!) By default, this will also prune any old backups as per the configured -retention policy, and check backups for consistency problems due to things -like file damage. +retention policy, compact segments to free up space (with Borg 1.2+), and +check backups for consistency problems due to things like file damage. The verbosity flag makes borgmatic show the steps it's performing. And the files flag lists each file that's new or changed since the last backup. diff --git a/tests/integration/borg/test_feature.py b/tests/integration/borg/test_feature.py new file mode 100644 index 000000000..9fdbc4993 --- /dev/null +++ b/tests/integration/borg/test_feature.py @@ -0,0 +1,13 @@ +from borgmatic.borg import feature as module + + +def test_available_true_for_new_enough_borg_version(): + assert module.available(module.Feature.COMPACT, '1.3.7') + + +def test_available_true_for_borg_version_introducing_feature(): + assert module.available(module.Feature.COMPACT, '1.2.0a2') + + +def test_available_false_for_too_old_borg_version(): + assert not module.available(module.Feature.COMPACT, '1.1.5') diff --git a/tests/unit/borg/test_compact.py b/tests/unit/borg/test_compact.py index d6170607c..ea13d9a36 100644 --- a/tests/unit/borg/test_compact.py +++ b/tests/unit/borg/test_compact.py @@ -17,33 +17,33 @@ COMPACT_COMMAND = ('borg', 'compact') def test_compact_segments_calls_borg_with_parameters(): - insert_execute_command_mock(COMPACT_COMMAND + ('repo',), logging.WARNING) + insert_execute_command_mock(COMPACT_COMMAND + ('repo',), logging.INFO) module.compact_segments(dry_run=False, repository='repo', storage_config={}) def test_compact_segments_with_log_info_calls_borg_with_info_parameter(): - insert_execute_command_mock(COMPACT_COMMAND + ('--info', 'repo'), logging.WARNING) + 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) def test_compact_segments_with_log_debug_calls_borg_with_debug_parameter(): - insert_execute_command_mock(COMPACT_COMMAND + ('--debug', '--show-rc', 'repo'), logging.WARNING) + 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) def test_compact_segments_with_dry_run_calls_borg_with_dry_run_parameter(): - insert_execute_command_mock(COMPACT_COMMAND + ('--dry-run', 'repo'), logging.WARNING) + insert_execute_command_mock(COMPACT_COMMAND + ('--dry-run', 'repo'), logging.INFO) module.compact_segments(repository='repo', storage_config={}, dry_run=True) def test_compact_segments_with_local_path_calls_borg_via_local_path(): - insert_execute_command_mock(('borg1',) + COMPACT_COMMAND[1:] + ('repo',), logging.WARNING) + insert_execute_command_mock(('borg1',) + COMPACT_COMMAND[1:] + ('repo',), logging.INFO) module.compact_segments( dry_run=False, repository='repo', storage_config={}, local_path='borg1', @@ -51,9 +51,7 @@ def test_compact_segments_with_local_path_calls_borg_via_local_path(): def test_compact_segments_with_remote_path_calls_borg_with_remote_path_parameters(): - insert_execute_command_mock( - COMPACT_COMMAND + ('--remote-path', 'borg1', 'repo'), logging.WARNING - ) + 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', @@ -61,7 +59,7 @@ def test_compact_segments_with_remote_path_calls_borg_with_remote_path_parameter def test_compact_segments_with_progress_calls_borg_with_progress_parameter(): - insert_execute_command_mock(COMPACT_COMMAND + ('--progress', 'repo'), logging.WARNING) + insert_execute_command_mock(COMPACT_COMMAND + ('--progress', 'repo'), logging.INFO) module.compact_segments( dry_run=False, repository='repo', storage_config={}, progress=True, @@ -69,7 +67,7 @@ def test_compact_segments_with_progress_calls_borg_with_progress_parameter(): def test_compact_segments_with_cleanup_commits_calls_borg_with_cleanup_commits_parameter(): - insert_execute_command_mock(COMPACT_COMMAND + ('--cleanup-commits', 'repo'), logging.WARNING) + insert_execute_command_mock(COMPACT_COMMAND + ('--cleanup-commits', 'repo'), logging.INFO) module.compact_segments( dry_run=False, repository='repo', storage_config={}, cleanup_commits=True, @@ -77,7 +75,7 @@ def test_compact_segments_with_cleanup_commits_calls_borg_with_cleanup_commits_p def test_compact_segments_with_threshold_calls_borg_with_threshold_parameter(): - insert_execute_command_mock(COMPACT_COMMAND + ('--threshold', '20', 'repo'), logging.WARNING) + insert_execute_command_mock(COMPACT_COMMAND + ('--threshold', '20', 'repo'), logging.INFO) module.compact_segments( dry_run=False, repository='repo', storage_config={}, threshold=20, @@ -86,7 +84,7 @@ def test_compact_segments_with_threshold_calls_borg_with_threshold_parameter(): def test_compact_segments_with_umask_calls_borg_with_umask_parameters(): storage_config = {'umask': '077'} - insert_execute_command_mock(COMPACT_COMMAND + ('--umask', '077', 'repo'), logging.WARNING) + insert_execute_command_mock(COMPACT_COMMAND + ('--umask', '077', 'repo'), logging.INFO) module.compact_segments( dry_run=False, repository='repo', storage_config=storage_config, @@ -95,7 +93,7 @@ def test_compact_segments_with_umask_calls_borg_with_umask_parameters(): def test_compact_segments_with_lock_wait_calls_borg_with_lock_wait_parameters(): storage_config = {'lock_wait': 5} - insert_execute_command_mock(COMPACT_COMMAND + ('--lock-wait', '5', 'repo'), logging.WARNING) + insert_execute_command_mock(COMPACT_COMMAND + ('--lock-wait', '5', 'repo'), logging.INFO) module.compact_segments( dry_run=False, repository='repo', storage_config=storage_config, @@ -103,7 +101,7 @@ def test_compact_segments_with_lock_wait_calls_borg_with_lock_wait_parameters(): def test_compact_segments_with_extra_borg_options_calls_borg_with_extra_options(): - insert_execute_command_mock(COMPACT_COMMAND + ('--extra', '--options', 'repo'), logging.WARNING) + insert_execute_command_mock(COMPACT_COMMAND + ('--extra', '--options', 'repo'), logging.INFO) module.compact_segments( dry_run=False, diff --git a/tests/unit/borg/test_version.py b/tests/unit/borg/test_version.py new file mode 100644 index 000000000..db4d2188f --- /dev/null +++ b/tests/unit/borg/test_version.py @@ -0,0 +1,49 @@ +import logging + +import pytest +from flexmock import flexmock + +from borgmatic.borg import version as module + +from ..test_verbosity import insert_logging_mock + +VERSION = '1.2.3' + + +def insert_execute_command_mock(command, borg_local_path='borg', version_output=f'borg {VERSION}'): + flexmock(module).should_receive('execute_command').with_args( + command, output_log_level=None, borg_local_path=borg_local_path + ).once().and_return(version_output) + + +def test_local_borg_version_calls_borg_with_required_parameters(): + insert_execute_command_mock(('borg', '--version')) + + assert module.local_borg_version() == VERSION + + +def test_local_borg_version_with_log_info_calls_borg_with_info_parameter(): + insert_execute_command_mock(('borg', '--version', '--info')) + insert_logging_mock(logging.INFO) + + assert module.local_borg_version() == VERSION + + +def test_local_borg_version_with_log_debug_calls_borg_with_debug_parameters(): + insert_execute_command_mock(('borg', '--version', '--debug', '--show-rc')) + insert_logging_mock(logging.DEBUG) + + assert module.local_borg_version() == VERSION + + +def test_local_borg_version_with_local_borg_path_calls_borg_with_it(): + insert_execute_command_mock(('borg1', '--version'), borg_local_path='borg1') + + assert module.local_borg_version('borg1') == VERSION + + +def test_local_borg_version_with_invalid_version_raises(): + insert_execute_command_mock(('borg', '--version'), version_output='wtf') + + with pytest.raises(ValueError): + module.local_borg_version() diff --git a/tests/unit/commands/test_arguments.py b/tests/unit/commands/test_arguments.py index 737cad3e5..640d6ade6 100644 --- a/tests/unit/commands/test_arguments.py +++ b/tests/unit/commands/test_arguments.py @@ -72,12 +72,14 @@ def test_parse_subparser_arguments_consumes_multiple_subparser_arguments(): def test_parse_subparser_arguments_applies_default_subparsers(): prune_namespace = flexmock() + compact_namespace = flexmock() create_namespace = flexmock(progress=True) check_namespace = flexmock() subparsers = { 'prune': flexmock( parse_known_args=lambda arguments: (prune_namespace, ['prune', '--progress']) ), + 'compact': flexmock(parse_known_args=lambda arguments: (compact_namespace, [])), 'create': flexmock(parse_known_args=lambda arguments: (create_namespace, [])), 'check': flexmock(parse_known_args=lambda arguments: (check_namespace, [])), 'other': flexmock(), @@ -87,6 +89,7 @@ def test_parse_subparser_arguments_applies_default_subparsers(): assert arguments == { 'prune': prune_namespace, + 'compact': compact_namespace, 'create': create_namespace, 'check': check_namespace, } diff --git a/tests/unit/commands/test_borgmatic.py b/tests/unit/commands/test_borgmatic.py index ba545f8e3..474f51726 100644 --- a/tests/unit/commands/test_borgmatic.py +++ b/tests/unit/commands/test_borgmatic.py @@ -10,6 +10,7 @@ from borgmatic.commands import borgmatic as module def test_run_configuration_runs_actions_for_each_repository(): flexmock(module.borg_environment).should_receive('initialize') + flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock()) expected_results = [flexmock(), flexmock()] flexmock(module).should_receive('run_actions').and_return(expected_results[:1]).and_return( expected_results[1:] @@ -22,8 +23,21 @@ def test_run_configuration_runs_actions_for_each_repository(): assert results == expected_results +def test_run_configuration_with_invalid_borg_version_errors(): + flexmock(module.borg_environment).should_receive('initialize') + flexmock(module.borg_version).should_receive('local_borg_version').and_raise(ValueError) + flexmock(module.command).should_receive('execute_hook').never() + flexmock(module.dispatch).should_receive('call_hooks').never() + flexmock(module).should_receive('run_actions').never() + 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_prune_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([]) @@ -35,6 +49,7 @@ def test_run_configuration_calls_hooks_for_prune_action(): 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']}} @@ -45,6 +60,7 @@ def test_run_configuration_calls_hooks_for_compact_action(): 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([]) @@ -56,6 +72,7 @@ def test_run_configuration_executes_and_calls_hooks_for_create_action(): def test_run_configuration_calls_hooks_for_check_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([]) @@ -67,6 +84,7 @@ def test_run_configuration_calls_hooks_for_check_action(): 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([]) @@ -78,6 +96,7 @@ def test_run_configuration_calls_hooks_for_extract_action(): 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([]) @@ -89,6 +108,7 @@ def test_run_configuration_does_not_trigger_hooks_for_list_action(): def test_run_configuration_logs_actions_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') flexmock(module.dispatch).should_receive('call_hooks') expected_results = [flexmock()] @@ -104,6 +124,7 @@ def test_run_configuration_logs_actions_error(): 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('make_error_log_records').and_return(expected_results) @@ -118,6 +139,7 @@ def test_run_configuration_logs_pre_hook_error(): def test_run_configuration_bails_for_pre_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_raise(error).and_return(None) flexmock(module).should_receive('make_error_log_records').never() @@ -132,6 +154,7 @@ def test_run_configuration_bails_for_pre_hook_soft_failure(): def test_run_configuration_logs_post_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_return(None).and_raise( OSError ).and_return(None) @@ -149,6 +172,7 @@ def test_run_configuration_logs_post_hook_error(): def test_run_configuration_bails_for_post_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 @@ -166,6 +190,7 @@ def test_run_configuration_bails_for_post_hook_soft_failure(): def test_run_configuration_logs_on_error_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) expected_results = [flexmock(), flexmock()] flexmock(module).should_receive('make_error_log_records').and_return( @@ -182,6 +207,7 @@ def test_run_configuration_logs_on_error_hook_error(): 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) expected_results = [flexmock()] @@ -198,6 +224,7 @@ def test_run_configuration_bails_for_on_error_hook_soft_failure(): def test_run_configuration_retries_soft_error(): # Run action first fails, second passes 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') flexmock(module).should_receive('run_actions').and_raise(OSError).and_return([]) expected_results = [flexmock()] @@ -211,6 +238,7 @@ def test_run_configuration_retries_soft_error(): def test_run_configuration_retries_hard_error(): # Run action fails twice 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') flexmock(module).should_receive('run_actions').and_raise(OSError).times(2) expected_results = [flexmock(), flexmock()] @@ -229,6 +257,7 @@ def test_run_configuration_retries_hard_error(): def test_run_repos_ordered(): 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') flexmock(module).should_receive('run_actions').and_raise(OSError).times(2) expected_results = [flexmock(), flexmock()] @@ -246,6 +275,7 @@ def test_run_repos_ordered(): def test_run_configuration_retries_round_robbin(): 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') flexmock(module).should_receive('run_actions').and_raise(OSError).times(4) expected_results = [flexmock(), flexmock(), flexmock(), flexmock()] @@ -269,6 +299,7 @@ def test_run_configuration_retries_round_robbin(): def test_run_configuration_retries_one_passes(): 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') flexmock(module).should_receive('run_actions').and_raise(OSError).and_raise(OSError).and_return( [] @@ -291,6 +322,7 @@ def test_run_configuration_retries_one_passes(): def test_run_configuration_retry_wait(): 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') flexmock(module).should_receive('run_actions').and_raise(OSError).times(4) expected_results = [flexmock(), flexmock(), flexmock(), flexmock()] @@ -320,6 +352,7 @@ def test_run_configuration_retry_wait(): def test_run_configuration_retries_timeout_multiple_repos(): 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') flexmock(module).should_receive('run_actions').and_raise(OSError).and_raise(OSError).and_return( []