diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..5d931745 --- /dev/null +++ b/.flake8 @@ -0,0 +1 @@ +select = Q0 diff --git a/NEWS b/NEWS index 39cb6c25..6eb4f3e4 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,22 @@ -1.7.9.dev0 +1.7.10.dev0 + * #501: Optionally error if a source directory does not exist via "source_directories_must_exist" + option in borgmatic's location configuration. + * #618: Support for BORG_FILES_CACHE_TTL environment variable via "borg_files_cache_ttl" option in + borgmatic's storage configuration. + +1.7.9 * #295: Add a SQLite database dump/restore hook. + * #304: Change the default action order when no actions are specified on the command-line to: + "create", "prune", "compact", "check". If you'd like to retain the old ordering ("prune" and + "compact" first), then specify actions explicitly on the command-line. + * #304: Run any command-line actions in the order specified instead of using a fixed ordering. + * #564: Add "--repository" flag to all actions where it makes sense, so you can run borgmatic on + a single configured repository instead of all of them. + * #628: Add a Healthchecks "log" state to send borgmatic logs to Healthchecks without signalling + success or failure. + * #647: Add "--strip-components all" feature on the "extract" action to remove leading path + components of files you extract. Must be used with the "--path" flag. + * Add support for Python 3.11. 1.7.8 * #620: With the "create" action and the "--list" ("--files") flag, only show excluded files at diff --git a/README.md b/README.md index 3b92dd37..27fc6cd2 100644 --- a/README.md +++ b/README.md @@ -81,8 +81,8 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/). Your first step is to [install and configure borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/). -For additional documentation, check out the links above for borgmatic how-to and +For additional documentation, check out the links above (left panel on wide screens) +for borgmatic how-to and reference guides. diff --git a/borgmatic/actions/check.py b/borgmatic/actions/check.py index 580c70ef..f3572395 100644 --- a/borgmatic/actions/check.py +++ b/borgmatic/actions/check.py @@ -1,6 +1,7 @@ import logging import borgmatic.borg.check +import borgmatic.config.validate import borgmatic.hooks.command logger = logging.getLogger(__name__) @@ -23,6 +24,11 @@ def run_check( ''' Run the "check" action for the given repository. ''' + if check_arguments.repository and not borgmatic.config.validate.repositories_match( + repository, check_arguments.repository + ): + return + borgmatic.hooks.command.execute_hook( hooks.get('before_check'), hooks.get('umask'), diff --git a/borgmatic/actions/compact.py b/borgmatic/actions/compact.py index 00585b0b..7a25b829 100644 --- a/borgmatic/actions/compact.py +++ b/borgmatic/actions/compact.py @@ -2,6 +2,7 @@ import logging import borgmatic.borg.compact import borgmatic.borg.feature +import borgmatic.config.validate import borgmatic.hooks.command logger = logging.getLogger(__name__) @@ -24,6 +25,11 @@ def run_compact( ''' Run the "compact" action for the given repository. ''' + if compact_arguments.repository and not borgmatic.config.validate.repositories_match( + repository, compact_arguments.repository + ): + return + borgmatic.hooks.command.execute_hook( hooks.get('before_compact'), hooks.get('umask'), diff --git a/borgmatic/actions/create.py b/borgmatic/actions/create.py index c882032a..96a48521 100644 --- a/borgmatic/actions/create.py +++ b/borgmatic/actions/create.py @@ -2,6 +2,7 @@ import json import logging import borgmatic.borg.create +import borgmatic.config.validate import borgmatic.hooks.command import borgmatic.hooks.dispatch import borgmatic.hooks.dump @@ -28,6 +29,11 @@ def run_create( If create_arguments.json is True, yield the JSON output from creating the archive. ''' + if create_arguments.repository and not borgmatic.config.validate.repositories_match( + repository, create_arguments.repository + ): + return + borgmatic.hooks.command.execute_hook( hooks.get('before_backup'), hooks.get('umask'), diff --git a/borgmatic/actions/prune.py b/borgmatic/actions/prune.py index 2d214a18..ca098ce4 100644 --- a/borgmatic/actions/prune.py +++ b/borgmatic/actions/prune.py @@ -1,6 +1,7 @@ import logging import borgmatic.borg.prune +import borgmatic.config.validate import borgmatic.hooks.command logger = logging.getLogger(__name__) @@ -23,6 +24,11 @@ def run_prune( ''' Run the "prune" action for the given repository. ''' + if prune_arguments.repository and not borgmatic.config.validate.repositories_match( + repository, prune_arguments.repository + ): + return + borgmatic.hooks.command.execute_hook( hooks.get('before_prune'), hooks.get('umask'), diff --git a/borgmatic/borg/check.py b/borgmatic/borg/check.py index 484a4154..d9beaa60 100644 --- a/borgmatic/borg/check.py +++ b/borgmatic/borg/check.py @@ -139,7 +139,7 @@ def filter_checks_on_frequency( if datetime.datetime.now() < check_time + frequency_delta: remaining = check_time + frequency_delta - datetime.datetime.now() logger.info( - f"Skipping {check} check due to configured frequency; {remaining} until next check" + f'Skipping {check} check due to configured frequency; {remaining} until next check' ) filtered_checks.remove(check) diff --git a/borgmatic/borg/create.py b/borgmatic/borg/create.py index 0889c3d1..87a0fdd7 100644 --- a/borgmatic/borg/create.py +++ b/borgmatic/borg/create.py @@ -306,6 +306,20 @@ def collect_special_file_paths( ) +def check_all_source_directories_exist(source_directories): + ''' + Given a sequence of source directories, check that they all exist. If any do not, raise an + exception. + ''' + missing_directories = [ + source_directory + for source_directory in source_directories + if not os.path.exists(source_directory) + ] + if missing_directories: + raise ValueError(f"Source directories do not exist: {', '.join(missing_directories)}") + + def create_archive( dry_run, repository, @@ -331,6 +345,8 @@ def create_archive( borgmatic_source_directories = expand_directories( collect_borgmatic_source_directories(location_config.get('borgmatic_source_directory')) ) + if location_config.get('source_directories_must_exist', False): + check_all_source_directories_exist(location_config.get('source_directories')) sources = deduplicate_directories( map_directories_to_devices( expand_directories( diff --git a/borgmatic/borg/environment.py b/borgmatic/borg/environment.py index 235c674d..1b14369a 100644 --- a/borgmatic/borg/environment.py +++ b/borgmatic/borg/environment.py @@ -2,6 +2,7 @@ OPTION_TO_ENVIRONMENT_VARIABLE = { 'borg_base_directory': 'BORG_BASE_DIR', 'borg_config_directory': 'BORG_CONFIG_DIR', 'borg_cache_directory': 'BORG_CACHE_DIR', + 'borg_files_cache_ttl': 'BORG_FILES_CACHE_TTL', 'borg_security_directory': 'BORG_SECURITY_DIR', 'borg_keys_directory': 'BORG_KEYS_DIR', 'encryption_passcommand': 'BORG_PASSCOMMAND', @@ -27,7 +28,7 @@ def make_environment(storage_config): value = storage_config.get(option_name) if value: - environment[environment_variable_name] = value + environment[environment_variable_name] = str(value) for ( option_name, diff --git a/borgmatic/borg/extract.py b/borgmatic/borg/extract.py index fccb0171..24014c91 100644 --- a/borgmatic/borg/extract.py +++ b/borgmatic/borg/extract.py @@ -87,6 +87,13 @@ def extract_archive( else: numeric_ids_flags = ('--numeric-owner',) if location_config.get('numeric_ids') else () + if strip_components == 'all': + if not paths: + raise ValueError('The --strip-components flag with "all" requires at least one --path') + + # Calculate the maximum number of leading path components of the given paths. + strip_components = max(0, *(len(path.split(os.path.sep)) - 1 for path in paths)) + full_command = ( (local_path, 'extract') + (('--remote-path', remote_path) if remote_path else ()) diff --git a/borgmatic/borg/rlist.py b/borgmatic/borg/rlist.py index 9f0de473..2a465fb0 100644 --- a/borgmatic/borg/rlist.py +++ b/borgmatic/borg/rlist.py @@ -17,7 +17,7 @@ def resolve_archive_name( Raise ValueError if "latest" is given but there are no archives in the repository. ''' - if archive != "latest": + if archive != 'latest': return archive lock_wait = storage_config.get('lock_wait', None) diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index 8af9bb56..d5dc6af4 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -46,11 +46,12 @@ def parse_subparser_arguments(unparsed_arguments, subparsers): if 'borg' in unparsed_arguments: subparsers = {'borg': subparsers['borg']} - for subparser_name, subparser in subparsers.items(): - if subparser_name not in remaining_arguments: - continue + for argument in remaining_arguments: + canonical_name = alias_to_subparser_name.get(argument, argument) + subparser = subparsers.get(canonical_name) - canonical_name = alias_to_subparser_name.get(subparser_name, subparser_name) + if not subparser: + continue # If a parsed value happens to be the same as the name of a subparser, remove it from the # remaining arguments. This prevents, for instance, "check --only extract" from triggering @@ -67,9 +68,9 @@ def parse_subparser_arguments(unparsed_arguments, subparsers): arguments[canonical_name] = parsed - # If no actions are explicitly requested, assume defaults: prune, compact, create, and check. + # If no actions are explicitly requested, assume defaults. if not arguments and '--help' not in unparsed_arguments and '-h' not in unparsed_arguments: - for subparser_name in ('prune', 'compact', 'create', 'check'): + for subparser_name in ('create', 'prune', 'compact', 'check'): subparser = subparsers[subparser_name] parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments) arguments[subparser_name] = parsed @@ -215,7 +216,7 @@ def make_parsers(): 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, compact, create, and + the action options are given, then borgmatic defaults to: create, prune, compact, and check. ''', parents=[global_parser], @@ -224,7 +225,7 @@ def make_parsers(): subparsers = top_level_parser.add_subparsers( title='actions', metavar='', - help='Specify zero or more actions. Defaults to prune, compact, create, and check. Use --help with action for details:', + help='Specify zero or more actions. Defaults to creat, prune, compact, and check. Use --help with action for details:', ) rcreate_parser = subparsers.add_parser( 'rcreate', @@ -332,6 +333,10 @@ def make_parsers(): add_help=False, ) prune_group = prune_parser.add_argument_group('prune arguments') + prune_group.add_argument( + '--repository', + help='Path of specific existing repository to prune (must be already specified in a borgmatic configuration file)', + ) prune_group.add_argument( '--stats', dest='stats', @@ -352,6 +357,10 @@ def make_parsers(): add_help=False, ) compact_group = compact_parser.add_argument_group('compact arguments') + compact_group.add_argument( + '--repository', + help='Path of specific existing repository to compact (must be already specified in a borgmatic configuration file)', + ) compact_group.add_argument( '--progress', dest='progress', @@ -384,6 +393,10 @@ def make_parsers(): add_help=False, ) create_group = create_parser.add_argument_group('create arguments') + create_group.add_argument( + '--repository', + help='Path of specific existing repository to backup to (must be already specified in a borgmatic configuration file)', + ) create_group.add_argument( '--progress', dest='progress', @@ -414,6 +427,10 @@ def make_parsers(): add_help=False, ) check_group = check_parser.add_argument_group('check arguments') + check_group.add_argument( + '--repository', + help='Path of specific existing repository to check (must be already specified in a borgmatic configuration file)', + ) check_group.add_argument( '--progress', dest='progress', @@ -475,10 +492,9 @@ def make_parsers(): ) extract_group.add_argument( '--strip-components', - type=int, + type=lambda number: number if number == 'all' else int(number), metavar='NUMBER', - dest='strip_components', - help='Number of leading path components to remove from each extracted path. Skip paths with fewer elements', + help='Number of leading path components to remove from each extracted path or "all" to strip all leading path components. Skip paths with fewer elements', ) extract_group.add_argument( '--progress', @@ -611,7 +627,7 @@ def make_parsers(): metavar='NAME', nargs='+', dest='databases', - help='Names of databases to restore from archive, defaults to all databases. Note that any databases to restore must be defined in borgmatic\'s configuration', + help="Names of databases to restore from archive, defaults to all databases. Note that any databases to restore must be defined in borgmatic's configuration", ) restore_group.add_argument( '-h', '--help', action='help', help='Show this help message and exit' @@ -805,7 +821,7 @@ def make_parsers(): 'borg', aliases=SUBPARSER_ALIASES['borg'], help='Run an arbitrary Borg command', - description='Run an arbitrary Borg command based on borgmatic\'s configuration', + description="Run an arbitrary Borg command based on borgmatic's configuration", add_help=False, ) borg_group = borg_parser.add_argument_group('borg arguments') diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index e08df976..fe07981e 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -44,8 +44,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 the defined prune, compact, - create, check, and/or other actions. + dict from subparser name to a namespace of parsed arguments, execute the defined create, prune, + compact, check, and/or other actions. Yield a combination of: @@ -64,7 +64,7 @@ def run_configuration(config_filename, config, arguments): retry_wait = storage.get('retry_wait', 0) encountered_error = None error_repository = '' - using_primary_action = {'prune', 'compact', 'create', 'check'}.intersection(arguments) + using_primary_action = {'create', 'prune', 'compact', 'check'}.intersection(arguments) monitoring_log_level = verbosity_to_log_level(global_arguments.monitoring_verbosity) try: @@ -152,6 +152,25 @@ def run_configuration(config_filename, config, arguments): encountered_error = error error_repository = repository_path + try: + if using_primary_action: + # send logs irrespective of error + dispatch.call_hooks( + 'ping_monitor', + hooks, + config_filename, + monitor.MONITOR_HOOK_NAMES, + monitor.State.LOG, + monitoring_log_level, + global_arguments.dry_run, + ) + except (OSError, CalledProcessError) as error: + if command.considered_soft_failure(config_filename, error): + return + + encountered_error = error + yield from log_error_records('{}: Error pinging monitor'.format(config_filename), error) + if not encountered_error: try: if using_primary_action: @@ -262,155 +281,162 @@ def run_actions( **hook_context, ) - if 'rcreate' in arguments: - borgmatic.actions.rcreate.run_rcreate( - repository, - storage, - local_borg_version, - arguments['rcreate'], - global_arguments, - local_path, - remote_path, - ) - if 'transfer' in arguments: - borgmatic.actions.transfer.run_transfer( - repository, - storage, - local_borg_version, - arguments['transfer'], - global_arguments, - local_path, - remote_path, - ) - if 'prune' in arguments: - borgmatic.actions.prune.run_prune( - config_filename, - repository, - storage, - retention, - hooks, - hook_context, - local_borg_version, - arguments['prune'], - global_arguments, - dry_run_label, - local_path, - remote_path, - ) - if 'compact' in arguments: - borgmatic.actions.compact.run_compact( - config_filename, - repository, - storage, - retention, - hooks, - hook_context, - local_borg_version, - arguments['compact'], - global_arguments, - dry_run_label, - local_path, - remote_path, - ) - if 'create' in arguments: - yield from borgmatic.actions.create.run_create( - config_filename, - repository, - location, - storage, - hooks, - hook_context, - local_borg_version, - arguments['create'], - global_arguments, - dry_run_label, - local_path, - remote_path, - ) - if 'check' in arguments and checks.repository_enabled_for_checks(repository, consistency): - borgmatic.actions.check.run_check( - config_filename, - repository, - location, - storage, - consistency, - hooks, - hook_context, - local_borg_version, - arguments['check'], - global_arguments, - local_path, - remote_path, - ) - if 'extract' in arguments: - borgmatic.actions.extract.run_extract( - config_filename, - repository, - location, - storage, - hooks, - hook_context, - local_borg_version, - arguments['extract'], - global_arguments, - local_path, - remote_path, - ) - if 'export-tar' in arguments: - borgmatic.actions.export_tar.run_export_tar( - repository, - storage, - local_borg_version, - arguments['export-tar'], - global_arguments, - local_path, - remote_path, - ) - if 'mount' in arguments: - borgmatic.actions.mount.run_mount( - repository, storage, local_borg_version, arguments['mount'], local_path, remote_path, - ) - if 'restore' in arguments: - borgmatic.actions.restore.run_restore( - repository, - location, - storage, - hooks, - local_borg_version, - arguments['restore'], - global_arguments, - local_path, - remote_path, - ) - if 'rlist' in arguments: - yield from borgmatic.actions.rlist.run_rlist( - repository, storage, local_borg_version, arguments['rlist'], local_path, remote_path, - ) - if 'list' in arguments: - yield from borgmatic.actions.list.run_list( - repository, storage, local_borg_version, arguments['list'], local_path, remote_path, - ) - if 'rinfo' in arguments: - yield from borgmatic.actions.rinfo.run_rinfo( - repository, storage, local_borg_version, arguments['rinfo'], local_path, remote_path, - ) - if 'info' in arguments: - yield from borgmatic.actions.info.run_info( - repository, storage, local_borg_version, arguments['info'], local_path, remote_path, - ) - if 'break-lock' in arguments: - borgmatic.actions.break_lock.run_break_lock( - repository, - storage, - local_borg_version, - arguments['break-lock'], - local_path, - remote_path, - ) - if 'borg' in arguments: - borgmatic.actions.borg.run_borg( - repository, storage, local_borg_version, arguments['borg'], local_path, remote_path, - ) + for (action_name, action_arguments) in arguments.items(): + if action_name == 'rcreate': + borgmatic.actions.rcreate.run_rcreate( + repository, + storage, + local_borg_version, + action_arguments, + global_arguments, + local_path, + remote_path, + ) + elif action_name == 'transfer': + borgmatic.actions.transfer.run_transfer( + repository, + storage, + local_borg_version, + action_arguments, + global_arguments, + local_path, + remote_path, + ) + elif action_name == 'create': + yield from borgmatic.actions.create.run_create( + config_filename, + repository, + location, + storage, + hooks, + hook_context, + local_borg_version, + action_arguments, + global_arguments, + dry_run_label, + local_path, + remote_path, + ) + elif action_name == 'prune': + borgmatic.actions.prune.run_prune( + config_filename, + repository, + storage, + retention, + hooks, + hook_context, + local_borg_version, + action_arguments, + global_arguments, + dry_run_label, + local_path, + remote_path, + ) + elif action_name == 'compact': + borgmatic.actions.compact.run_compact( + config_filename, + repository, + storage, + retention, + hooks, + hook_context, + local_borg_version, + action_arguments, + global_arguments, + dry_run_label, + local_path, + remote_path, + ) + elif action_name == 'check': + if checks.repository_enabled_for_checks(repository, consistency): + borgmatic.actions.check.run_check( + config_filename, + repository, + location, + storage, + consistency, + hooks, + hook_context, + local_borg_version, + action_arguments, + global_arguments, + local_path, + remote_path, + ) + elif action_name == 'extract': + borgmatic.actions.extract.run_extract( + config_filename, + repository, + location, + storage, + hooks, + hook_context, + local_borg_version, + action_arguments, + global_arguments, + local_path, + remote_path, + ) + elif action_name == 'export-tar': + borgmatic.actions.export_tar.run_export_tar( + repository, + storage, + local_borg_version, + action_arguments, + global_arguments, + local_path, + remote_path, + ) + elif action_name == 'mount': + borgmatic.actions.mount.run_mount( + repository, + storage, + local_borg_version, + arguments['mount'], + local_path, + remote_path, + ) + elif action_name == 'restore': + borgmatic.actions.restore.run_restore( + repository, + location, + storage, + hooks, + local_borg_version, + action_arguments, + global_arguments, + local_path, + remote_path, + ) + elif action_name == 'rlist': + yield from borgmatic.actions.rlist.run_rlist( + repository, storage, local_borg_version, action_arguments, local_path, remote_path, + ) + elif action_name == 'list': + yield from borgmatic.actions.list.run_list( + repository, storage, local_borg_version, action_arguments, local_path, remote_path, + ) + elif action_name == 'rinfo': + yield from borgmatic.actions.rinfo.run_rinfo( + repository, storage, local_borg_version, action_arguments, local_path, remote_path, + ) + elif action_name == 'info': + yield from borgmatic.actions.info.run_info( + repository, storage, local_borg_version, action_arguments, local_path, remote_path, + ) + elif action_name == 'break-lock': + borgmatic.actions.break_lock.run_break_lock( + repository, + storage, + local_borg_version, + arguments['break-lock'], + local_path, + remote_path, + ) + elif action_name == 'borg': + borgmatic.actions.borg.run_borg( + repository, storage, local_borg_version, action_arguments, local_path, remote_path, + ) command.execute_hook( hooks.get('after_actions'), diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index 2f873b7a..d4d57ab6 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -202,6 +202,12 @@ properties: path prevents "borgmatic restore" from finding any database dumps created before the change. Defaults to ~/.borgmatic example: /tmp/borgmatic + source_directories_must_exist: + type: boolean + description: | + If true, then source directories must exist, otherwise an + error is raised. Defaults to false. + example: true storage: type: object description: | @@ -315,6 +321,12 @@ properties: Path for Borg cache files. Defaults to $borg_base_directory/.cache/borg example: /path/to/base/cache + borg_files_cache_ttl: + type: integer + description: | + Maximum time to live (ttl) for entries in the Borg files + cache. + example: 20 borg_security_directory: type: string description: | @@ -369,6 +381,11 @@ properties: description: | Extra command-line options to pass to "borg init". example: "--extra-option" + create: + type: string + description: | + Extra command-line options to pass to "borg create". + example: "--extra-option" prune: type: string description: | @@ -379,11 +396,6 @@ properties: description: | Extra command-line options to pass to "borg compact". example: "--extra-option" - create: - type: string - description: | - Extra command-line options to pass to "borg create". - example: "--extra-option" check: type: string description: | @@ -663,11 +675,11 @@ properties: type: string description: | List of one or more shell commands or scripts to execute - when an exception occurs during a "prune", "compact", - "create", or "check" action or an associated before/after + when an exception occurs during a "create", "prune", + "compact", or "check" action or an associated before/after hook. example: - - echo "Error during prune/compact/create/check." + - echo "Error during create/prune/compact/check." before_everything: type: array items: @@ -951,9 +963,9 @@ properties: name: type: string description: | - This is used to tag the database dump file - with a name. It is not the path to the database - file itself. The name "all" has no special + This is used to tag the database dump file + with a name. It is not the path to the database + file itself. The name "all" has no special meaning for SQLite databases. example: users path: @@ -1168,7 +1180,7 @@ properties: type: string description: | Healthchecks ping URL or UUID to notify when a - backup begins, ends, or errors. + backup begins, ends, errors or just to send logs. example: https://hc-ping.com/your-uuid-here verify_tls: type: boolean @@ -1180,7 +1192,8 @@ properties: type: boolean description: | Send borgmatic logs to Healthchecks as part the - "finish" state. Defaults to true. + "finish", "fail", and "log" states. Defaults to + true. example: false ping_body_limit: type: integer @@ -1199,10 +1212,11 @@ properties: - start - finish - fail + - log uniqueItems: true description: | List of one or more monitoring states to ping for: - "start", "finish", and/or "fail". Defaults to + "start", "finish", "fail", and/or "log". Defaults to pinging for all states. example: - finish diff --git a/borgmatic/config/validate.py b/borgmatic/config/validate.py index d61f7f3a..d25fb562 100644 --- a/borgmatic/config/validate.py +++ b/borgmatic/config/validate.py @@ -189,5 +189,5 @@ def guard_single_repository_selected(repository, configurations): if count != 1: raise ValueError( - 'Can\'t determine which repository to use. Use --repository to disambiguate' + "Can't determine which repository to use. Use --repository to disambiguate" ) diff --git a/borgmatic/hooks/cronhub.py b/borgmatic/hooks/cronhub.py index b93788e4..cd0ffa5c 100644 --- a/borgmatic/hooks/cronhub.py +++ b/borgmatic/hooks/cronhub.py @@ -27,6 +27,12 @@ def ping_monitor(hook_config, config_filename, state, monitoring_log_level, dry_ Ping the configured Cronhub URL, modified with the monitor.State. Use the given configuration filename in any log entries. If this is a dry run, then don't actually ping anything. ''' + if state not in MONITOR_STATE_TO_CRONHUB: + logger.debug( + f'{config_filename}: Ignoring unsupported monitoring {state.name.lower()} in Cronhub hook' + ) + return + dry_run_label = ' (dry run; not actually pinging)' if dry_run else '' formatted_state = '/{}/'.format(MONITOR_STATE_TO_CRONHUB[state]) ping_url = ( diff --git a/borgmatic/hooks/cronitor.py b/borgmatic/hooks/cronitor.py index 8866a6ac..633b4c3c 100644 --- a/borgmatic/hooks/cronitor.py +++ b/borgmatic/hooks/cronitor.py @@ -27,6 +27,12 @@ def ping_monitor(hook_config, config_filename, state, monitoring_log_level, dry_ Ping the configured Cronitor URL, modified with the monitor.State. Use the given configuration filename in any log entries. If this is a dry run, then don't actually ping anything. ''' + if state not in MONITOR_STATE_TO_CRONITOR: + logger.debug( + f'{config_filename}: Ignoring unsupported monitoring {state.name.lower()} in Cronitor hook' + ) + return + dry_run_label = ' (dry run; not actually pinging)' if dry_run else '' ping_url = '{}/{}'.format(hook_config['ping_url'], MONITOR_STATE_TO_CRONITOR[state]) diff --git a/borgmatic/hooks/healthchecks.py b/borgmatic/hooks/healthchecks.py index 03d012a8..6ad8449f 100644 --- a/borgmatic/hooks/healthchecks.py +++ b/borgmatic/hooks/healthchecks.py @@ -10,6 +10,7 @@ MONITOR_STATE_TO_HEALTHCHECKS = { monitor.State.START: 'start', monitor.State.FINISH: None, # Healthchecks doesn't append to the URL for the finished state. monitor.State.FAIL: 'fail', + monitor.State.LOG: 'log', } PAYLOAD_TRUNCATION_INDICATOR = '...\n' @@ -117,7 +118,7 @@ def ping_monitor(hook_config, config_filename, state, monitoring_log_level, dry_ ) logger.debug('{}: Using Healthchecks ping URL {}'.format(config_filename, ping_url)) - if state in (monitor.State.FINISH, monitor.State.FAIL): + if state in (monitor.State.FINISH, monitor.State.FAIL, monitor.State.LOG): payload = format_buffered_logs_for_payload() else: payload = '' diff --git a/borgmatic/hooks/monitor.py b/borgmatic/hooks/monitor.py index 846fca15..c0168178 100644 --- a/borgmatic/hooks/monitor.py +++ b/borgmatic/hooks/monitor.py @@ -7,3 +7,4 @@ class State(Enum): START = 1 FINISH = 2 FAIL = 3 + LOG = 4 diff --git a/borgmatic/hooks/ntfy.py b/borgmatic/hooks/ntfy.py index 99ed254a..8a6f0fb8 100644 --- a/borgmatic/hooks/ntfy.py +++ b/borgmatic/hooks/ntfy.py @@ -2,16 +2,8 @@ import logging import requests -from borgmatic.hooks import monitor - logger = logging.getLogger(__name__) -MONITOR_STATE_TO_NTFY = { - monitor.State.START: None, - monitor.State.FINISH: None, - monitor.State.FAIL: None, -} - def initialize_monitor( ping_url, config_filename, monitoring_log_level, dry_run diff --git a/docs/Dockerfile b/docs/Dockerfile index ef29da82..8800cc1f 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -4,7 +4,7 @@ COPY . /app RUN apk add --no-cache py3-pip py3-ruamel.yaml py3-ruamel.yaml.clib RUN pip install --no-cache /app && generate-borgmatic-config && chmod +r /etc/borgmatic/config.yaml RUN borgmatic --help > /command-line.txt \ - && for action in rcreate transfer prune compact create check extract export-tar mount umount restore rlist list rinfo info break-lock borg; do \ + && for action in rcreate transfer create prune compact check extract export-tar mount umount restore rlist list rinfo info break-lock borg; do \ echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \ && borgmatic "$action" --help >> /command-line.txt; done diff --git a/docs/how-to/backup-your-databases.md b/docs/how-to/backup-your-databases.md index 602f6488..bc21b659 100644 --- a/docs/how-to/backup-your-databases.md +++ b/docs/how-to/backup-your-databases.md @@ -316,7 +316,10 @@ user and you're extracting to `/tmp`, then the dump will be in `/tmp/root/.borgmatic`. After extraction, you can manually restore the dump file using native database -commands like `pg_restore`, `mysql`, `mongorestore` or similar. +commands like `pg_restore`, `mysql`, `mongorestore`, `sqlite`, or similar. + +Also see the documentation on [listing database +dumps](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/#listing-database-dumps). ## Preparation and cleanup hooks diff --git a/docs/how-to/deal-with-very-large-backups.md b/docs/how-to/deal-with-very-large-backups.md index 720d4334..e5962c1e 100644 --- a/docs/how-to/deal-with-very-large-backups.md +++ b/docs/how-to/deal-with-very-large-backups.md @@ -9,37 +9,47 @@ 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`, `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. +find that while borgmatic's default actions of `create`, `prune`, `compact`, +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. + +Prior to version 1.7.9 The +default action ordering was `prune`, `compact`, `create`, and `check`. ### A la carte actions -If you find yourself in this situation, you have some options. First, you can -run borgmatic's `prune`, `compact`, `create`, or `check` actions separately. -For instance, the following optional actions are available: +If you find yourself wanting to customize the actions, you have some options. +First, you can run borgmatic's `prune`, `compact`, `create`, or `check` +actions separately. For instance, the following optional actions are +available (among others): ```bash +borgmatic create borgmatic prune borgmatic compact -borgmatic create 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 `compact` and only runs `create` and `check`: +You can run borgmatic 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 `compact` and only runs `create` and `check`: ```bash borgmatic create check ``` -Or, you can make backups with `create` on a frequent schedule (e.g. with -`borgmatic create` called from one cron job), while only running expensive -consistency checks with `check` on a much less frequent basis (e.g. with -`borgmatic check` called from a separate cron job). +New in version 1.7.9 borgmatic +now respects your specified command-line action order, running actions in the +order you specify. In previous versions, borgmatic ran your specified actions +in a fixed ordering regardless of the order they appeared on the command-line. + +But instead of running actions together, another option is to run backups with +`create` on a frequent schedule (e.g. with `borgmatic create` called from one +cron job), while only running expensive consistency checks with `check` on a +much less frequent basis (e.g. with `borgmatic check` called from a separate +cron job). ### Consistency check configuration @@ -47,8 +57,8 @@ consistency checks with `check` on a much less frequent basis (e.g. with Another option is to customize your consistency checks. By default, if you omit consistency checks from configuration, borgmatic runs full-repository checks (`repository`) and per-archive checks (`archives`) within each -repository, no more than once a month. This is equivalent to what `borg check` -does if run without options. +repository. (Although see below about check frequency.) This is equivalent to +what `borg check` does if run without options. But if you find that archive checks are too slow, for example, you can configure borgmatic to run repository checks only. Configure this in the @@ -60,8 +70,9 @@ consistency: - name: repository ``` -Prior to version 1.6.2 `checks` -was a plain list of strings without the `name:` part. For example: +Prior to version 1.6.2 The +`checks` option was a plain list of strings without the `name:` part, and +borgmatic ran each configured check every time checks were run. For example: ```yaml consistency: @@ -102,8 +113,13 @@ consistency: This tells borgmatic to run the `repository` consistency check at most once every two weeks for a given repository and the `archives` check at most once a month. The `frequency` value is a number followed by a unit of time, e.g. "3 -days", "1 week", "2 months", etc. The `frequency` defaults to `always`, which -means run this check every time checks run. +days", "1 week", "2 months", etc. + +The `frequency` defaults to `always` for a check configured without a +`frequency`, which means run this check every time checks run. But if you omit +consistency checks from configuration entirely, borgmatic runs full-repository +checks (`repository`) and per-archive checks (`archives`) within each +repository, at most once a month. Unlike a real scheduler like cron, borgmatic only makes a best effort to run checks on the configured frequency. It compares that frequency with how long diff --git a/docs/how-to/develop-on-borgmatic.md b/docs/how-to/develop-on-borgmatic.md index 80ecf304..fbc1d244 100644 --- a/docs/how-to/develop-on-borgmatic.md +++ b/docs/how-to/develop-on-borgmatic.md @@ -26,7 +26,7 @@ make sure your changes work. ```bash cd borgmatic/ -pip3 install --editable --user . +pip3 install --user --editable . ``` Note that this will typically install the borgmatic commands into diff --git a/docs/how-to/extract-a-backup.md b/docs/how-to/extract-a-backup.md index 62bdc75b..4285c784 100644 --- a/docs/how-to/extract-a-backup.md +++ b/docs/how-to/extract-a-backup.md @@ -20,15 +20,15 @@ borgmatic rlist That should yield output looking something like: ```text -host-2019-01-01T04:05:06.070809 Tue, 2019-01-01 04:05:06 [...] -host-2019-01-02T04:06:07.080910 Wed, 2019-01-02 04:06:07 [...] +host-2023-01-01T04:05:06.070809 Tue, 2023-01-01 04:05:06 [...] +host-2023-01-02T04:06:07.080910 Wed, 2023-01-02 04:06:07 [...] ``` Assuming that you want to extract the archive with the most up-to-date files and therefore the latest timestamp, run a command like: ```bash -borgmatic extract --archive host-2019-01-02T04:06:07.080910 +borgmatic extract --archive host-2023-01-02T04:06:07.080910 ``` (No borgmatic `extract` action? Upgrade borgmatic!) @@ -54,7 +54,7 @@ But if you have multiple repositories configured, then you'll need to specify the repository path containing the archive to extract. Here's an example: ```bash -borgmatic extract --repository repo.borg --archive host-2019-... +borgmatic extract --repository repo.borg --archive host-2023-... ``` ## Extract particular files @@ -74,6 +74,13 @@ run the `extract` command above, borgmatic will extract `/var/path/1` and `/var/path/2`. +### Searching for files + +If you're not sure which archive contains the files you're looking for, you +can [search across +archives](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/#searching-for-a-file). + + ## Extract to a particular destination By default, borgmatic extracts files into the current directory. To instead diff --git a/docs/how-to/inspect-your-backups.md b/docs/how-to/inspect-your-backups.md index bace6ad9..57a2381c 100644 --- a/docs/how-to/inspect-your-backups.md +++ b/docs/how-to/inspect-your-backups.md @@ -91,6 +91,19 @@ example, to search only the last five archives: borgmatic list --find foo.txt --last 5 ``` +## Listing database dumps + +If you have enabled borgmatic's [database +hooks](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/), you +can list backed up database dumps via borgmatic. For example: + +```bash +borgmatic list --archive latest --find .borgmatic/*_databases +``` + +This gives you a listing of all database dump files contained in the latest +archive, complete with file sizes. + ## Logging diff --git a/docs/how-to/monitor-your-backups.md b/docs/how-to/monitor-your-backups.md index 575f4af4..eb7a6200 100644 --- a/docs/how-to/monitor-your-backups.md +++ b/docs/how-to/monitor-your-backups.md @@ -83,7 +83,7 @@ tests](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/). ## Error hooks -When an error occurs during a `prune`, `compact`, `create`, or `check` action, +When an error occurs during a `create`, `prune`, `compact`, 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: @@ -116,8 +116,8 @@ the repository. Here's the full set of supported variables you can use 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`, `compact`, -`create`, or `check` actions or hooks in which an error occurs, and not other +Note that borgmatic runs the `on_error` hooks only for `create`, `prune`, +`compact`, 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 @@ -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`, `compact`, `create`, or `check` actions are run. +the `create`, `prune`, `compact`, 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 @@ -154,8 +154,8 @@ 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`, -`compact`, `create`, or `check` action is run. +itself. But the logs are only included for errors that occur when a `create`, +`prune`, `compact`, 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 `--list` and `--stats` flags diff --git a/docs/how-to/set-up-backups.md b/docs/how-to/set-up-backups.md index be133e34..52962c34 100644 --- a/docs/how-to/set-up-backups.md +++ b/docs/how-to/set-up-backups.md @@ -94,6 +94,7 @@ installing borgmatic: * [openSUSE](https://software.opensuse.org/package/borgmatic) * [macOS (via Homebrew)](https://formulae.brew.sh/formula/borgmatic) * [macOS (via MacPorts)](https://ports.macports.org/port/borgmatic/) + * [NixOS](https://search.nixos.org/packages?show=borgmatic&sort=relevance&type=packages&query=borgmatic) * [Ansible role](https://github.com/borgbase/ansible-role-borgbackup) * [virtualenv](https://virtualenv.pypa.io/en/stable/) diff --git a/setup.cfg b/setup.cfg index fe6236b6..d8d28f89 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,6 +10,8 @@ filterwarnings = [flake8] ignore = E501,W503 exclude = *.*/* +multiline-quotes = ''' +docstring-quotes = ''' [tool:isort] force_single_line = False diff --git a/setup.py b/setup.py index 7ddb8117..5ea3c2e4 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import find_packages, setup -VERSION = '1.7.9.dev0' +VERSION = '1.7.10.dev0' setup( diff --git a/test_requirements.txt b/test_requirements.txt index e47c6fd6..9cae8fb4 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -5,6 +5,7 @@ click==7.1.2; python_version >= '3.8' colorama==0.4.4 coverage==5.3 flake8==4.0.1 +flake8-quotes==3.3.2 flexmock==0.10.4 isort==5.9.1 mccabe==0.6.1 diff --git a/tests/integration/commands/test_arguments.py b/tests/integration/commands/test_arguments.py index cb001c10..754564b2 100644 --- a/tests/integration/commands/test_arguments.py +++ b/tests/integration/commands/test_arguments.py @@ -254,13 +254,6 @@ def test_parse_arguments_allows_init_and_create(): module.parse_arguments('--config', 'myconfig', 'init', '--encryption', 'repokey', 'create') -def test_parse_arguments_disallows_repository_unless_action_consumes_it(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - with pytest.raises(SystemExit): - module.parse_arguments('--config', 'myconfig', '--repository', 'test.borg') - - def test_parse_arguments_allows_repository_with_extract(): flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) diff --git a/tests/unit/actions/test_check.py b/tests/unit/actions/test_check.py index 0007ee3f..3e1a9c2f 100644 --- a/tests/unit/actions/test_check.py +++ b/tests/unit/actions/test_check.py @@ -3,15 +3,78 @@ from flexmock import flexmock from borgmatic.actions import check as module -def test_run_check_calls_hooks(): +def test_run_check_calls_hooks_for_configured_repository(): flexmock(module.logger).answer = lambda message: None flexmock(module.borgmatic.config.checks).should_receive( 'repository_enabled_for_checks' ).and_return(True) - flexmock(module.borgmatic.borg.check).should_receive('check_archives') + flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never() + flexmock(module.borgmatic.borg.check).should_receive('check_archives').once() flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2) check_arguments = flexmock( - progress=flexmock(), repair=flexmock(), only=flexmock(), force=flexmock() + repository=None, progress=flexmock(), repair=flexmock(), only=flexmock(), force=flexmock(), + ) + global_arguments = flexmock(monitoring_verbosity=1, dry_run=False) + + module.run_check( + config_filename='test.yaml', + repository='repo', + location={'repositories': ['repo']}, + storage={}, + consistency={}, + hooks={}, + hook_context={}, + local_borg_version=None, + check_arguments=check_arguments, + global_arguments=global_arguments, + local_path=None, + remote_path=None, + ) + + +def test_run_check_runs_with_selected_repository(): + flexmock(module.logger).answer = lambda message: None + flexmock(module.borgmatic.config.validate).should_receive( + 'repositories_match' + ).once().and_return(True) + flexmock(module.borgmatic.borg.check).should_receive('check_archives').once() + check_arguments = flexmock( + repository=flexmock(), + progress=flexmock(), + repair=flexmock(), + only=flexmock(), + force=flexmock(), + ) + global_arguments = flexmock(monitoring_verbosity=1, dry_run=False) + + module.run_check( + config_filename='test.yaml', + repository=flexmock(), + location={'repositories': ['repo']}, + storage={}, + consistency={}, + hooks={}, + hook_context={}, + local_borg_version=None, + check_arguments=check_arguments, + global_arguments=global_arguments, + local_path=None, + remote_path=None, + ) + + +def test_run_check_bails_if_repository_does_not_match(): + flexmock(module.logger).answer = lambda message: None + flexmock(module.borgmatic.config.validate).should_receive( + 'repositories_match' + ).once().and_return(False) + flexmock(module.borgmatic.borg.check).should_receive('check_archives').never() + check_arguments = flexmock( + repository=flexmock(), + progress=flexmock(), + repair=flexmock(), + only=flexmock(), + force=flexmock(), ) global_arguments = flexmock(monitoring_verbosity=1, dry_run=False) diff --git a/tests/unit/actions/test_compact.py b/tests/unit/actions/test_compact.py index bc2b9406..4dae903e 100644 --- a/tests/unit/actions/test_compact.py +++ b/tests/unit/actions/test_compact.py @@ -3,13 +3,70 @@ from flexmock import flexmock from borgmatic.actions import compact as module -def test_compact_actions_calls_hooks(): +def test_compact_actions_calls_hooks_for_configured_repository(): flexmock(module.logger).answer = lambda message: None flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True) - flexmock(module.borgmatic.borg.compact).should_receive('compact_segments') + flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never() + flexmock(module.borgmatic.borg.compact).should_receive('compact_segments').once() flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2) compact_arguments = flexmock( - progress=flexmock(), cleanup_commits=flexmock(), threshold=flexmock() + repository=None, progress=flexmock(), cleanup_commits=flexmock(), threshold=flexmock() + ) + global_arguments = flexmock(monitoring_verbosity=1, dry_run=False) + + module.run_compact( + config_filename='test.yaml', + repository='repo', + storage={}, + retention={}, + hooks={}, + hook_context={}, + local_borg_version=None, + compact_arguments=compact_arguments, + global_arguments=global_arguments, + dry_run_label='', + local_path=None, + remote_path=None, + ) + + +def test_compact_runs_with_selected_repository(): + flexmock(module.logger).answer = lambda message: None + flexmock(module.borgmatic.config.validate).should_receive( + 'repositories_match' + ).once().and_return(True) + flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True) + flexmock(module.borgmatic.borg.compact).should_receive('compact_segments').once() + compact_arguments = flexmock( + repository=flexmock(), progress=flexmock(), cleanup_commits=flexmock(), threshold=flexmock() + ) + global_arguments = flexmock(monitoring_verbosity=1, dry_run=False) + + module.run_compact( + config_filename='test.yaml', + repository='repo', + storage={}, + retention={}, + hooks={}, + hook_context={}, + local_borg_version=None, + compact_arguments=compact_arguments, + global_arguments=global_arguments, + dry_run_label='', + local_path=None, + remote_path=None, + ) + + +def test_compact_bails_if_repository_does_not_match(): + flexmock(module.logger).answer = lambda message: None + flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True) + flexmock(module.borgmatic.config.validate).should_receive( + 'repositories_match' + ).once().and_return(False) + flexmock(module.borgmatic.borg.compact).should_receive('compact_segments').never() + compact_arguments = flexmock( + repository=flexmock(), progress=flexmock(), cleanup_commits=flexmock(), threshold=flexmock() ) global_arguments = flexmock(monitoring_verbosity=1, dry_run=False) diff --git a/tests/unit/actions/test_create.py b/tests/unit/actions/test_create.py index 915f0ae1..8a9d0b4e 100644 --- a/tests/unit/actions/test_create.py +++ b/tests/unit/actions/test_create.py @@ -3,16 +3,87 @@ from flexmock import flexmock from borgmatic.actions import create as module -def test_run_create_executes_and_calls_hooks(): +def test_run_create_executes_and_calls_hooks_for_configured_repository(): flexmock(module.logger).answer = lambda message: None - flexmock(module.borgmatic.borg.create).should_receive('create_archive') + flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never() + flexmock(module.borgmatic.borg.create).should_receive('create_archive').once() flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2) flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return({}) flexmock(module.borgmatic.hooks.dispatch).should_receive( 'call_hooks_even_if_unconfigured' ).and_return({}) create_arguments = flexmock( - progress=flexmock(), stats=flexmock(), json=flexmock(), list_files=flexmock() + repository=None, + progress=flexmock(), + stats=flexmock(), + json=flexmock(), + list_files=flexmock(), + ) + global_arguments = flexmock(monitoring_verbosity=1, dry_run=False) + + list( + module.run_create( + config_filename='test.yaml', + repository='repo', + location={}, + storage={}, + hooks={}, + hook_context={}, + local_borg_version=None, + create_arguments=create_arguments, + global_arguments=global_arguments, + dry_run_label='', + local_path=None, + remote_path=None, + ) + ) + + +def test_run_create_runs_with_selected_repository(): + flexmock(module.logger).answer = lambda message: None + flexmock(module.borgmatic.config.validate).should_receive( + 'repositories_match' + ).once().and_return(True) + flexmock(module.borgmatic.borg.create).should_receive('create_archive').once() + create_arguments = flexmock( + repository=flexmock(), + progress=flexmock(), + stats=flexmock(), + json=flexmock(), + list_files=flexmock(), + ) + global_arguments = flexmock(monitoring_verbosity=1, dry_run=False) + + list( + module.run_create( + config_filename='test.yaml', + repository='repo', + location={}, + storage={}, + hooks={}, + hook_context={}, + local_borg_version=None, + create_arguments=create_arguments, + global_arguments=global_arguments, + dry_run_label='', + local_path=None, + remote_path=None, + ) + ) + + +def test_run_create_bails_if_repository_does_not_match(): + flexmock(module.logger).answer = lambda message: None + flexmock(module.borgmatic.config.validate).should_receive( + 'repositories_match' + ).once().and_return(False) + flexmock(module.borgmatic.borg.create).should_receive('create_archive').never() + create_arguments = flexmock( + repository=flexmock(), + progress=flexmock(), + stats=flexmock(), + json=flexmock(), + list_files=flexmock(), ) global_arguments = flexmock(monitoring_verbosity=1, dry_run=False) diff --git a/tests/unit/actions/test_prune.py b/tests/unit/actions/test_prune.py index d34a9c84..db9c1247 100644 --- a/tests/unit/actions/test_prune.py +++ b/tests/unit/actions/test_prune.py @@ -3,11 +3,62 @@ from flexmock import flexmock from borgmatic.actions import prune as module -def test_run_prune_calls_hooks(): +def test_run_prune_calls_hooks_for_configured_repository(): flexmock(module.logger).answer = lambda message: None - flexmock(module.borgmatic.borg.prune).should_receive('prune_archives') + flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never() + flexmock(module.borgmatic.borg.prune).should_receive('prune_archives').once() flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2) - prune_arguments = flexmock(stats=flexmock(), list_archives=flexmock()) + prune_arguments = flexmock(repository=None, stats=flexmock(), list_archives=flexmock()) + global_arguments = flexmock(monitoring_verbosity=1, dry_run=False) + + module.run_prune( + config_filename='test.yaml', + repository='repo', + storage={}, + retention={}, + hooks={}, + hook_context={}, + local_borg_version=None, + prune_arguments=prune_arguments, + global_arguments=global_arguments, + dry_run_label='', + local_path=None, + remote_path=None, + ) + + +def test_run_prune_runs_with_selected_repository(): + flexmock(module.logger).answer = lambda message: None + flexmock(module.borgmatic.config.validate).should_receive( + 'repositories_match' + ).once().and_return(True) + flexmock(module.borgmatic.borg.prune).should_receive('prune_archives').once() + prune_arguments = flexmock(repository=flexmock(), stats=flexmock(), list_archives=flexmock()) + global_arguments = flexmock(monitoring_verbosity=1, dry_run=False) + + module.run_prune( + config_filename='test.yaml', + repository='repo', + storage={}, + retention={}, + hooks={}, + hook_context={}, + local_borg_version=None, + prune_arguments=prune_arguments, + global_arguments=global_arguments, + dry_run_label='', + local_path=None, + remote_path=None, + ) + + +def test_run_prune_bails_if_repository_does_not_match(): + flexmock(module.logger).answer = lambda message: None + flexmock(module.borgmatic.config.validate).should_receive( + 'repositories_match' + ).once().and_return(False) + flexmock(module.borgmatic.borg.prune).should_receive('prune_archives').never() + prune_arguments = flexmock(repository=flexmock(), stats=flexmock(), list_archives=flexmock()) global_arguments = flexmock(monitoring_verbosity=1, dry_run=False) module.run_prune( diff --git a/tests/unit/borg/test_create.py b/tests/unit/borg/test_create.py index 3c4e4338..69a3ede2 100644 --- a/tests/unit/borg/test_create.py +++ b/tests/unit/borg/test_create.py @@ -207,7 +207,6 @@ def test_make_exclude_flags_includes_exclude_patterns_filename_when_given(): def test_make_exclude_flags_includes_exclude_from_filenames_when_in_config(): - exclude_flags = module.make_exclude_flags( location_config={'exclude_from': ['excludes', 'other']} ) @@ -1916,7 +1915,7 @@ def test_create_archive_with_stream_processes_ignores_read_special_false_and_log (f'repo::{DEFAULT_ARCHIVE_NAME}',) ) flexmock(module.environment).should_receive('make_environment') - flexmock(module).should_receive('collect_special_file_paths').and_return(("/dev/null",)) + flexmock(module).should_receive('collect_special_file_paths').and_return(('/dev/null',)) create_command = ( 'borg', 'create', @@ -2530,3 +2529,27 @@ def test_create_archive_with_stream_processes_calls_borg_with_processes_and_read local_borg_version='1.2.3', stream_processes=processes, ) + + +def test_create_archive_with_non_existent_directory_and_source_directories_must_exist_raises_error(): + ''' + If a source directory doesn't exist and source_directories_must_exist is True, raise an error. + ''' + flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels') + flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER + flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([]) + flexmock(module.os.path).should_receive('exists').and_return(False) + + with pytest.raises(ValueError): + module.create_archive( + dry_run=False, + repository='repo', + location_config={ + 'source_directories': ['foo', 'bar'], + 'repositories': ['repo'], + 'exclude_patterns': None, + 'source_directories_must_exist': True, + }, + storage_config={}, + local_borg_version='1.2.3', + ) diff --git a/tests/unit/borg/test_environment.py b/tests/unit/borg/test_environment.py index c5fce80d..4cef39b9 100644 --- a/tests/unit/borg/test_environment.py +++ b/tests/unit/borg/test_environment.py @@ -32,3 +32,8 @@ def test_make_environment_with_relocated_repo_access_should_override_default(): environment = module.make_environment({'relocated_repo_access_is_ok': True}) assert environment.get('BORG_RELOCATED_REPO_ACCESS_IS_OK') == 'yes' + + +def test_make_environment_with_integer_variable_value(): + environment = module.make_environment({'borg_files_cache_ttl': 40}) + assert environment.get('BORG_FILES_CACHE_TTL') == '40' diff --git a/tests/unit/borg/test_extract.py b/tests/unit/borg/test_extract.py index 64d16d35..d27026e4 100644 --- a/tests/unit/borg/test_extract.py +++ b/tests/unit/borg/test_extract.py @@ -312,6 +312,57 @@ def test_extract_archive_calls_borg_with_strip_components(): ) +def test_extract_archive_calls_borg_with_strip_components_calculated_from_all(): + flexmock(module.os.path).should_receive('abspath').and_return('repo') + insert_execute_command_mock( + ( + 'borg', + 'extract', + '--strip-components', + '2', + 'repo::archive', + 'foo/bar/baz.txt', + 'foo/bar.txt', + ) + ) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) + + module.extract_archive( + dry_run=False, + repository='repo', + archive='archive', + paths=['foo/bar/baz.txt', 'foo/bar.txt'], + location_config={}, + storage_config={}, + local_borg_version='1.2.3', + strip_components='all', + ) + + +def test_extract_archive_with_strip_components_all_and_no_paths_raises(): + flexmock(module.os.path).should_receive('abspath').and_return('repo') + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) + flexmock(module).should_receive('execute_command').never() + + with pytest.raises(ValueError): + module.extract_archive( + dry_run=False, + repository='repo', + archive='archive', + paths=None, + location_config={}, + storage_config={}, + local_borg_version='1.2.3', + strip_components='all', + ) + + def test_extract_archive_calls_borg_with_progress_parameter(): flexmock(module.os.path).should_receive('abspath').and_return('repo') flexmock(module.environment).should_receive('make_environment') diff --git a/tests/unit/commands/test_arguments.py b/tests/unit/commands/test_arguments.py index 640d6ade..9354cf5e 100644 --- a/tests/unit/commands/test_arguments.py +++ b/tests/unit/commands/test_arguments.py @@ -1,3 +1,5 @@ +import collections + from flexmock import flexmock from borgmatic.commands import arguments as module @@ -70,6 +72,26 @@ def test_parse_subparser_arguments_consumes_multiple_subparser_arguments(): assert remaining_arguments == [] +def test_parse_subparser_arguments_respects_command_line_action_ordering(): + other_namespace = flexmock() + action_namespace = flexmock(foo=True) + subparsers = { + 'action': flexmock( + parse_known_args=lambda arguments: (action_namespace, ['action', '--foo', 'true']) + ), + 'other': flexmock(parse_known_args=lambda arguments: (other_namespace, ['other'])), + } + + arguments, remaining_arguments = module.parse_subparser_arguments( + ('other', '--foo', 'true', 'action'), subparsers + ) + + assert arguments == collections.OrderedDict( + [('other', other_namespace), ('action', action_namespace)] + ) + assert remaining_arguments == [] + + def test_parse_subparser_arguments_applies_default_subparsers(): prune_namespace = flexmock() compact_namespace = flexmock() diff --git a/tests/unit/commands/test_borgmatic.py b/tests/unit/commands/test_borgmatic.py index 38b28fd9..19ac00de 100644 --- a/tests/unit/commands/test_borgmatic.py +++ b/tests/unit/commands/test_borgmatic.py @@ -40,7 +40,7 @@ def test_run_configuration_logs_monitor_start_error(): flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock()) flexmock(module.dispatch).should_receive('call_hooks').and_raise(OSError).and_return( None - ).and_return(None) + ).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() @@ -99,7 +99,7 @@ def test_run_configuration_bails_for_actions_soft_failure(): assert results == [] -def test_run_configuration_logs_monitor_finish_error(): +def test_run_configuration_logs_monitor_log_error(): flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO) flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock()) flexmock(module.dispatch).should_receive('call_hooks').and_return(None).and_return( @@ -116,13 +116,48 @@ def test_run_configuration_logs_monitor_finish_error(): assert results == expected_results +def test_run_configuration_bails_for_monitor_log_soft_failure(): + flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO) + 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.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()} + + results = list(module.run_configuration('test.yaml', config, arguments)) + + assert results == [] + + +def test_run_configuration_logs_monitor_finish_error(): + flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO) + flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock()) + flexmock(module.dispatch).should_receive('call_hooks').and_return(None).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([]) + 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_monitor_finish_soft_failure(): flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO) 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.dispatch).should_receive('call_hooks').and_return(None).and_return( None - ).and_raise(error) + ).and_raise(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) @@ -401,6 +436,30 @@ def test_run_actions_runs_transfer(): ) +def test_run_actions_runs_create(): + flexmock(module).should_receive('add_custom_log_levels') + flexmock(module.command).should_receive('execute_hook') + expected = flexmock() + flexmock(borgmatic.actions.create).should_receive('run_create').and_yield(expected).once() + + result = tuple( + module.run_actions( + arguments={'global': flexmock(dry_run=False), 'create': flexmock()}, + config_filename=flexmock(), + location={'repositories': []}, + storage=flexmock(), + retention=flexmock(), + consistency=flexmock(), + hooks={}, + local_path=flexmock(), + remote_path=flexmock(), + local_borg_version=flexmock(), + repository_path='repo', + ) + ) + assert result == (expected,) + + def test_run_actions_runs_prune(): flexmock(module).should_receive('add_custom_log_levels') flexmock(module.command).should_receive('execute_hook') @@ -445,30 +504,6 @@ def test_run_actions_runs_compact(): ) -def test_run_actions_runs_create(): - flexmock(module).should_receive('add_custom_log_levels') - flexmock(module.command).should_receive('execute_hook') - expected = flexmock() - flexmock(borgmatic.actions.create).should_receive('run_create').and_yield(expected).once() - - result = tuple( - module.run_actions( - arguments={'global': flexmock(dry_run=False), 'create': flexmock()}, - config_filename=flexmock(), - location={'repositories': []}, - storage=flexmock(), - retention=flexmock(), - consistency=flexmock(), - hooks={}, - local_path=flexmock(), - remote_path=flexmock(), - local_borg_version=flexmock(), - repository_path='repo', - ) - ) - assert result == (expected,) - - def test_run_actions_runs_check_when_repository_enabled_for_checks(): flexmock(module).should_receive('add_custom_log_levels') flexmock(module.command).should_receive('execute_hook') @@ -743,6 +778,33 @@ def test_run_actions_runs_borg(): ) +def test_run_actions_runs_multiple_actions_in_argument_order(): + flexmock(module).should_receive('add_custom_log_levels') + flexmock(module.command).should_receive('execute_hook') + flexmock(borgmatic.actions.borg).should_receive('run_borg').once().ordered() + flexmock(borgmatic.actions.restore).should_receive('run_restore').once().ordered() + + tuple( + module.run_actions( + arguments={ + 'global': flexmock(dry_run=False), + 'borg': flexmock(), + 'restore': flexmock(), + }, + config_filename=flexmock(), + location={'repositories': []}, + storage=flexmock(), + retention=flexmock(), + consistency=flexmock(), + hooks={}, + local_path=flexmock(), + remote_path=flexmock(), + local_borg_version=flexmock(), + repository_path='repo', + ) + ) + + def test_load_configurations_collects_parsed_configurations_and_logs(): configuration = flexmock() other_configuration = flexmock() diff --git a/tests/unit/hooks/test_cronhub.py b/tests/unit/hooks/test_cronhub.py index 14e8eb23..f470b88e 100644 --- a/tests/unit/hooks/test_cronhub.py +++ b/tests/unit/hooks/test_cronhub.py @@ -102,3 +102,11 @@ def test_ping_monitor_with_other_error_logs_warning(): monitoring_log_level=1, dry_run=False, ) + + +def test_ping_monitor_with_unsupported_monitoring_state(): + hook_config = {'ping_url': 'https://example.com'} + flexmock(module.requests).should_receive('get').never() + module.ping_monitor( + hook_config, 'config.yaml', module.monitor.State.LOG, monitoring_log_level=1, dry_run=False, + ) diff --git a/tests/unit/hooks/test_cronitor.py b/tests/unit/hooks/test_cronitor.py index 4b762d85..7ec1e2e6 100644 --- a/tests/unit/hooks/test_cronitor.py +++ b/tests/unit/hooks/test_cronitor.py @@ -87,3 +87,11 @@ def test_ping_monitor_with_other_error_logs_warning(): monitoring_log_level=1, dry_run=False, ) + + +def test_ping_monitor_with_unsupported_monitoring_state(): + hook_config = {'ping_url': 'https://example.com'} + flexmock(module.requests).should_receive('get').never() + module.ping_monitor( + hook_config, 'config.yaml', module.monitor.State.LOG, monitoring_log_level=1, dry_run=False, + ) diff --git a/tests/unit/hooks/test_healthchecks.py b/tests/unit/hooks/test_healthchecks.py index ee78e52b..d5779534 100644 --- a/tests/unit/hooks/test_healthchecks.py +++ b/tests/unit/hooks/test_healthchecks.py @@ -184,6 +184,23 @@ def test_ping_monitor_hits_ping_url_for_fail_state(): ) +def test_ping_monitor_hits_ping_url_for_log_state(): + hook_config = {'ping_url': 'https://example.com'} + payload = 'data' + flexmock(module).should_receive('format_buffered_logs_for_payload').and_return(payload) + flexmock(module.requests).should_receive('post').with_args( + 'https://example.com/log', data=payload.encode('utf'), verify=True + ).and_return(flexmock(ok=True)) + + module.ping_monitor( + hook_config, + 'config.yaml', + state=module.monitor.State.LOG, + monitoring_log_level=1, + dry_run=False, + ) + + def test_ping_monitor_with_ping_uuid_hits_corresponding_url(): hook_config = {'ping_url': 'abcd-efgh-ijkl-mnop'} payload = 'data' diff --git a/tests/unit/hooks/test_mongodb.py b/tests/unit/hooks/test_mongodb.py index 9544a09f..f61f3c70 100644 --- a/tests/unit/hooks/test_mongodb.py +++ b/tests/unit/hooks/test_mongodb.py @@ -72,7 +72,7 @@ def test_dump_databases_runs_mongodump_with_username_and_password(): 'name': 'foo', 'username': 'mongo', 'password': 'trustsome1', - 'authentication_database': "admin", + 'authentication_database': 'admin', } ] process = flexmock() diff --git a/tests/unit/hooks/test_ntfy.py b/tests/unit/hooks/test_ntfy.py index ea3f3c1d..9731df7a 100644 --- a/tests/unit/hooks/test_ntfy.py +++ b/tests/unit/hooks/test_ntfy.py @@ -2,6 +2,7 @@ from enum import Enum from flexmock import flexmock +import borgmatic.hooks.monitor from borgmatic.hooks import ntfy as module default_base_url = 'https://ntfy.sh' @@ -37,12 +38,16 @@ def test_ping_monitor_minimal_config_hits_hosted_ntfy_on_fail(): hook_config = {'topic': topic} flexmock(module.requests).should_receive('post').with_args( f'{default_base_url}/{topic}', - headers=return_default_message_headers(module.monitor.State.FAIL), + headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL), auth=None, ).and_return(flexmock(ok=True)).once() module.ping_monitor( - hook_config, 'config.yaml', module.monitor.State.FAIL, monitoring_log_level=1, dry_run=False + hook_config, + 'config.yaml', + borgmatic.hooks.monitor.State.FAIL, + monitoring_log_level=1, + dry_run=False, ) @@ -54,12 +59,16 @@ def test_ping_monitor_with_auth_hits_hosted_ntfy_on_fail(): } flexmock(module.requests).should_receive('post').with_args( f'{default_base_url}/{topic}', - headers=return_default_message_headers(module.monitor.State.FAIL), + headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL), auth=module.requests.auth.HTTPBasicAuth('testuser', 'fakepassword'), ).and_return(flexmock(ok=True)).once() module.ping_monitor( - hook_config, 'config.yaml', module.monitor.State.FAIL, monitoring_log_level=1, dry_run=False + hook_config, + 'config.yaml', + borgmatic.hooks.monitor.State.FAIL, + monitoring_log_level=1, + dry_run=False, ) @@ -67,13 +76,17 @@ def test_ping_monitor_auth_with_no_username_warning(): hook_config = {'topic': topic, 'password': 'fakepassword'} flexmock(module.requests).should_receive('post').with_args( f'{default_base_url}/{topic}', - headers=return_default_message_headers(module.monitor.State.FAIL), + headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL), auth=None, ).and_return(flexmock(ok=True)).once() flexmock(module.logger).should_receive('warning').once() module.ping_monitor( - hook_config, 'config.yaml', module.monitor.State.FAIL, monitoring_log_level=1, dry_run=False + hook_config, + 'config.yaml', + borgmatic.hooks.monitor.State.FAIL, + monitoring_log_level=1, + dry_run=False, ) @@ -81,13 +94,17 @@ def test_ping_monitor_auth_with_no_password_warning(): hook_config = {'topic': topic, 'username': 'testuser'} flexmock(module.requests).should_receive('post').with_args( f'{default_base_url}/{topic}', - headers=return_default_message_headers(module.monitor.State.FAIL), + headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL), auth=None, ).and_return(flexmock(ok=True)).once() flexmock(module.logger).should_receive('warning').once() module.ping_monitor( - hook_config, 'config.yaml', module.monitor.State.FAIL, monitoring_log_level=1, dry_run=False + hook_config, + 'config.yaml', + borgmatic.hooks.monitor.State.FAIL, + monitoring_log_level=1, + dry_run=False, ) @@ -98,7 +115,7 @@ def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_start(): module.ping_monitor( hook_config, 'config.yaml', - module.monitor.State.START, + borgmatic.hooks.monitor.State.START, monitoring_log_level=1, dry_run=False, ) @@ -111,7 +128,7 @@ def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_finish(): module.ping_monitor( hook_config, 'config.yaml', - module.monitor.State.FINISH, + borgmatic.hooks.monitor.State.FINISH, monitoring_log_level=1, dry_run=False, ) @@ -121,12 +138,16 @@ def test_ping_monitor_minimal_config_hits_selfhosted_ntfy_on_fail(): hook_config = {'topic': topic, 'server': custom_base_url} flexmock(module.requests).should_receive('post').with_args( f'{custom_base_url}/{topic}', - headers=return_default_message_headers(module.monitor.State.FAIL), + headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL), auth=None, ).and_return(flexmock(ok=True)).once() module.ping_monitor( - hook_config, 'config.yaml', module.monitor.State.FAIL, monitoring_log_level=1, dry_run=False + hook_config, + 'config.yaml', + borgmatic.hooks.monitor.State.FAIL, + monitoring_log_level=1, + dry_run=False, ) @@ -135,7 +156,11 @@ def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_fail_dry_run(): flexmock(module.requests).should_receive('post').never() module.ping_monitor( - hook_config, 'config.yaml', module.monitor.State.FAIL, monitoring_log_level=1, dry_run=True + hook_config, + 'config.yaml', + borgmatic.hooks.monitor.State.FAIL, + monitoring_log_level=1, + dry_run=True, ) @@ -146,7 +171,11 @@ def test_ping_monitor_custom_message_hits_hosted_ntfy_on_fail(): ).and_return(flexmock(ok=True)).once() module.ping_monitor( - hook_config, 'config.yaml', module.monitor.State.FAIL, monitoring_log_level=1, dry_run=False + hook_config, + 'config.yaml', + borgmatic.hooks.monitor.State.FAIL, + monitoring_log_level=1, + dry_run=False, ) @@ -154,14 +183,14 @@ def test_ping_monitor_custom_state_hits_hosted_ntfy_on_start(): hook_config = {'topic': topic, 'states': ['start', 'fail']} flexmock(module.requests).should_receive('post').with_args( f'{default_base_url}/{topic}', - headers=return_default_message_headers(module.monitor.State.START), + headers=return_default_message_headers(borgmatic.hooks.monitor.State.START), auth=None, ).and_return(flexmock(ok=True)).once() module.ping_monitor( hook_config, 'config.yaml', - module.monitor.State.START, + borgmatic.hooks.monitor.State.START, monitoring_log_level=1, dry_run=False, ) @@ -171,7 +200,7 @@ def test_ping_monitor_with_connection_error_logs_warning(): hook_config = {'topic': topic} flexmock(module.requests).should_receive('post').with_args( f'{default_base_url}/{topic}', - headers=return_default_message_headers(module.monitor.State.FAIL), + headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL), auth=None, ).and_raise(module.requests.exceptions.ConnectionError) flexmock(module.logger).should_receive('warning').once() @@ -179,7 +208,7 @@ def test_ping_monitor_with_connection_error_logs_warning(): module.ping_monitor( hook_config, 'config.yaml', - module.monitor.State.FAIL, + borgmatic.hooks.monitor.State.FAIL, monitoring_log_level=1, dry_run=False, ) @@ -193,7 +222,7 @@ def test_ping_monitor_with_other_error_logs_warning(): ) flexmock(module.requests).should_receive('post').with_args( f'{default_base_url}/{topic}', - headers=return_default_message_headers(module.monitor.State.FAIL), + headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL), auth=None, ).and_return(response) flexmock(module.logger).should_receive('warning').once() @@ -201,7 +230,7 @@ def test_ping_monitor_with_other_error_logs_warning(): module.ping_monitor( hook_config, 'config.yaml', - module.monitor.State.FAIL, + borgmatic.hooks.monitor.State.FAIL, monitoring_log_level=1, dry_run=False, ) diff --git a/tox.ini b/tox.ini index a58c871d..17b7a9da 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,py38,py39,py310 +envlist = py37,py38,py39,py310,py311 skip_missing_interpreters = True skipsdist = True minversion = 3.14.1 @@ -13,7 +13,7 @@ whitelist_externals = passenv = COVERAGE_FILE commands = pytest {posargs} - py38,py39,py310: black --check . + py38,py39,py310,py311: black --check . isort --check-only --settings-path setup.cfg . flake8 borgmatic tests