Merge branch 'master' of https://github.com/diivi/borgmatic into feat/file-urls-support

This commit is contained in:
Divyansh Singh 2023-03-21 16:55:05 +05:30
commit e86d223bbf
48 changed files with 933 additions and 302 deletions

1
.flake8 Normal file
View file

@ -0,0 +1 @@
select = Q0

19
NEWS
View file

@ -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. * #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 1.7.8
* #620: With the "create" action and the "--list" ("--files") flag, only show excluded files at * #620: With the "create" action and the "--list" ("--files") flag, only show excluded files at

View file

@ -81,8 +81,8 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
Your first step is to [install and configure Your first step is to [install and configure
borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/). borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/).
For additional documentation, check out the links above for <a For additional documentation, check out the links above (left panel on wide screens)
href="https://torsion.org/borgmatic/#documentation">borgmatic how-to and for <a href="https://torsion.org/borgmatic/#documentation">borgmatic how-to and
reference guides</a>. reference guides</a>.

View file

@ -1,6 +1,7 @@
import logging import logging
import borgmatic.borg.check import borgmatic.borg.check
import borgmatic.config.validate
import borgmatic.hooks.command import borgmatic.hooks.command
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -23,6 +24,11 @@ def run_check(
''' '''
Run the "check" action for the given repository. 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( borgmatic.hooks.command.execute_hook(
hooks.get('before_check'), hooks.get('before_check'),
hooks.get('umask'), hooks.get('umask'),

View file

@ -2,6 +2,7 @@ import logging
import borgmatic.borg.compact import borgmatic.borg.compact
import borgmatic.borg.feature import borgmatic.borg.feature
import borgmatic.config.validate
import borgmatic.hooks.command import borgmatic.hooks.command
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -24,6 +25,11 @@ def run_compact(
''' '''
Run the "compact" action for the given repository. 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( borgmatic.hooks.command.execute_hook(
hooks.get('before_compact'), hooks.get('before_compact'),
hooks.get('umask'), hooks.get('umask'),

View file

@ -2,6 +2,7 @@ import json
import logging import logging
import borgmatic.borg.create import borgmatic.borg.create
import borgmatic.config.validate
import borgmatic.hooks.command import borgmatic.hooks.command
import borgmatic.hooks.dispatch import borgmatic.hooks.dispatch
import borgmatic.hooks.dump 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.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( borgmatic.hooks.command.execute_hook(
hooks.get('before_backup'), hooks.get('before_backup'),
hooks.get('umask'), hooks.get('umask'),

View file

@ -1,6 +1,7 @@
import logging import logging
import borgmatic.borg.prune import borgmatic.borg.prune
import borgmatic.config.validate
import borgmatic.hooks.command import borgmatic.hooks.command
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -23,6 +24,11 @@ def run_prune(
''' '''
Run the "prune" action for the given repository. 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( borgmatic.hooks.command.execute_hook(
hooks.get('before_prune'), hooks.get('before_prune'),
hooks.get('umask'), hooks.get('umask'),

View file

@ -139,7 +139,7 @@ def filter_checks_on_frequency(
if datetime.datetime.now() < check_time + frequency_delta: if datetime.datetime.now() < check_time + frequency_delta:
remaining = check_time + frequency_delta - datetime.datetime.now() remaining = check_time + frequency_delta - datetime.datetime.now()
logger.info( 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) filtered_checks.remove(check)

View file

@ -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( def create_archive(
dry_run, dry_run,
repository, repository,
@ -331,6 +345,8 @@ def create_archive(
borgmatic_source_directories = expand_directories( borgmatic_source_directories = expand_directories(
collect_borgmatic_source_directories(location_config.get('borgmatic_source_directory')) 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( sources = deduplicate_directories(
map_directories_to_devices( map_directories_to_devices(
expand_directories( expand_directories(

View file

@ -2,6 +2,7 @@ OPTION_TO_ENVIRONMENT_VARIABLE = {
'borg_base_directory': 'BORG_BASE_DIR', 'borg_base_directory': 'BORG_BASE_DIR',
'borg_config_directory': 'BORG_CONFIG_DIR', 'borg_config_directory': 'BORG_CONFIG_DIR',
'borg_cache_directory': 'BORG_CACHE_DIR', 'borg_cache_directory': 'BORG_CACHE_DIR',
'borg_files_cache_ttl': 'BORG_FILES_CACHE_TTL',
'borg_security_directory': 'BORG_SECURITY_DIR', 'borg_security_directory': 'BORG_SECURITY_DIR',
'borg_keys_directory': 'BORG_KEYS_DIR', 'borg_keys_directory': 'BORG_KEYS_DIR',
'encryption_passcommand': 'BORG_PASSCOMMAND', 'encryption_passcommand': 'BORG_PASSCOMMAND',
@ -27,7 +28,7 @@ def make_environment(storage_config):
value = storage_config.get(option_name) value = storage_config.get(option_name)
if value: if value:
environment[environment_variable_name] = value environment[environment_variable_name] = str(value)
for ( for (
option_name, option_name,

View file

@ -87,6 +87,13 @@ def extract_archive(
else: else:
numeric_ids_flags = ('--numeric-owner',) if location_config.get('numeric_ids') 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 = ( full_command = (
(local_path, 'extract') (local_path, 'extract')
+ (('--remote-path', remote_path) if remote_path else ()) + (('--remote-path', remote_path) if remote_path else ())

View file

@ -17,7 +17,7 @@ def resolve_archive_name(
Raise ValueError if "latest" is given but there are no archives in the repository. Raise ValueError if "latest" is given but there are no archives in the repository.
''' '''
if archive != "latest": if archive != 'latest':
return archive return archive
lock_wait = storage_config.get('lock_wait', None) lock_wait = storage_config.get('lock_wait', None)

View file

@ -46,11 +46,12 @@ def parse_subparser_arguments(unparsed_arguments, subparsers):
if 'borg' in unparsed_arguments: if 'borg' in unparsed_arguments:
subparsers = {'borg': subparsers['borg']} subparsers = {'borg': subparsers['borg']}
for subparser_name, subparser in subparsers.items(): for argument in remaining_arguments:
if subparser_name not in remaining_arguments: canonical_name = alias_to_subparser_name.get(argument, argument)
continue 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 # 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 # 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 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: 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] subparser = subparsers[subparser_name]
parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments) parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments)
arguments[subparser_name] = parsed arguments[subparser_name] = parsed
@ -215,7 +216,7 @@ def make_parsers():
top_level_parser = ArgumentParser( top_level_parser = ArgumentParser(
description=''' description='''
Simple, configuration-driven backup software for servers and workstations. If none of 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. check.
''', ''',
parents=[global_parser], parents=[global_parser],
@ -224,7 +225,7 @@ def make_parsers():
subparsers = top_level_parser.add_subparsers( subparsers = top_level_parser.add_subparsers(
title='actions', title='actions',
metavar='', 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_parser = subparsers.add_parser(
'rcreate', 'rcreate',
@ -332,6 +333,10 @@ def make_parsers():
add_help=False, add_help=False,
) )
prune_group = prune_parser.add_argument_group('prune arguments') 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( prune_group.add_argument(
'--stats', '--stats',
dest='stats', dest='stats',
@ -352,6 +357,10 @@ def make_parsers():
add_help=False, add_help=False,
) )
compact_group = compact_parser.add_argument_group('compact arguments') 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( compact_group.add_argument(
'--progress', '--progress',
dest='progress', dest='progress',
@ -384,6 +393,10 @@ def make_parsers():
add_help=False, add_help=False,
) )
create_group = create_parser.add_argument_group('create arguments') 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( create_group.add_argument(
'--progress', '--progress',
dest='progress', dest='progress',
@ -414,6 +427,10 @@ def make_parsers():
add_help=False, add_help=False,
) )
check_group = check_parser.add_argument_group('check arguments') 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( check_group.add_argument(
'--progress', '--progress',
dest='progress', dest='progress',
@ -475,10 +492,9 @@ def make_parsers():
) )
extract_group.add_argument( extract_group.add_argument(
'--strip-components', '--strip-components',
type=int, type=lambda number: number if number == 'all' else int(number),
metavar='NUMBER', metavar='NUMBER',
dest='strip_components', 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',
help='Number of leading path components to remove from each extracted path. Skip paths with fewer elements',
) )
extract_group.add_argument( extract_group.add_argument(
'--progress', '--progress',
@ -611,7 +627,7 @@ def make_parsers():
metavar='NAME', metavar='NAME',
nargs='+', nargs='+',
dest='databases', 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( restore_group.add_argument(
'-h', '--help', action='help', help='Show this help message and exit' '-h', '--help', action='help', help='Show this help message and exit'
@ -805,7 +821,7 @@ def make_parsers():
'borg', 'borg',
aliases=SUBPARSER_ALIASES['borg'], aliases=SUBPARSER_ALIASES['borg'],
help='Run an arbitrary Borg command', 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, add_help=False,
) )
borg_group = borg_parser.add_argument_group('borg arguments') borg_group = borg_parser.add_argument_group('borg arguments')

View file

@ -44,8 +44,8 @@ LEGACY_CONFIG_PATH = '/etc/borgmatic/config'
def run_configuration(config_filename, config, arguments): def run_configuration(config_filename, config, arguments):
''' '''
Given a config filename, the corresponding parsed config dict, and command-line arguments as a 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, dict from subparser name to a namespace of parsed arguments, execute the defined create, prune,
create, check, and/or other actions. compact, check, and/or other actions.
Yield a combination of: Yield a combination of:
@ -64,7 +64,7 @@ def run_configuration(config_filename, config, arguments):
retry_wait = storage.get('retry_wait', 0) retry_wait = storage.get('retry_wait', 0)
encountered_error = None encountered_error = None
error_repository = '' 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) monitoring_log_level = verbosity_to_log_level(global_arguments.monitoring_verbosity)
try: try:
@ -152,6 +152,25 @@ def run_configuration(config_filename, config, arguments):
encountered_error = error encountered_error = error
error_repository = repository_path 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: if not encountered_error:
try: try:
if using_primary_action: if using_primary_action:
@ -262,155 +281,162 @@ def run_actions(
**hook_context, **hook_context,
) )
if 'rcreate' in arguments: for (action_name, action_arguments) in arguments.items():
borgmatic.actions.rcreate.run_rcreate( if action_name == 'rcreate':
repository, borgmatic.actions.rcreate.run_rcreate(
storage, repository,
local_borg_version, storage,
arguments['rcreate'], local_borg_version,
global_arguments, action_arguments,
local_path, global_arguments,
remote_path, local_path,
) remote_path,
if 'transfer' in arguments: )
borgmatic.actions.transfer.run_transfer( elif action_name == 'transfer':
repository, borgmatic.actions.transfer.run_transfer(
storage, repository,
local_borg_version, storage,
arguments['transfer'], local_borg_version,
global_arguments, action_arguments,
local_path, global_arguments,
remote_path, local_path,
) remote_path,
if 'prune' in arguments: )
borgmatic.actions.prune.run_prune( elif action_name == 'create':
config_filename, yield from borgmatic.actions.create.run_create(
repository, config_filename,
storage, repository,
retention, location,
hooks, storage,
hook_context, hooks,
local_borg_version, hook_context,
arguments['prune'], local_borg_version,
global_arguments, action_arguments,
dry_run_label, global_arguments,
local_path, dry_run_label,
remote_path, local_path,
) remote_path,
if 'compact' in arguments: )
borgmatic.actions.compact.run_compact( elif action_name == 'prune':
config_filename, borgmatic.actions.prune.run_prune(
repository, config_filename,
storage, repository,
retention, storage,
hooks, retention,
hook_context, hooks,
local_borg_version, hook_context,
arguments['compact'], local_borg_version,
global_arguments, action_arguments,
dry_run_label, global_arguments,
local_path, dry_run_label,
remote_path, local_path,
) remote_path,
if 'create' in arguments: )
yield from borgmatic.actions.create.run_create( elif action_name == 'compact':
config_filename, borgmatic.actions.compact.run_compact(
repository, config_filename,
location, repository,
storage, storage,
hooks, retention,
hook_context, hooks,
local_borg_version, hook_context,
arguments['create'], local_borg_version,
global_arguments, action_arguments,
dry_run_label, global_arguments,
local_path, dry_run_label,
remote_path, local_path,
) remote_path,
if 'check' in arguments and checks.repository_enabled_for_checks(repository, consistency): )
borgmatic.actions.check.run_check( elif action_name == 'check':
config_filename, if checks.repository_enabled_for_checks(repository, consistency):
repository, borgmatic.actions.check.run_check(
location, config_filename,
storage, repository,
consistency, location,
hooks, storage,
hook_context, consistency,
local_borg_version, hooks,
arguments['check'], hook_context,
global_arguments, local_borg_version,
local_path, action_arguments,
remote_path, global_arguments,
) local_path,
if 'extract' in arguments: remote_path,
borgmatic.actions.extract.run_extract( )
config_filename, elif action_name == 'extract':
repository, borgmatic.actions.extract.run_extract(
location, config_filename,
storage, repository,
hooks, location,
hook_context, storage,
local_borg_version, hooks,
arguments['extract'], hook_context,
global_arguments, local_borg_version,
local_path, action_arguments,
remote_path, global_arguments,
) local_path,
if 'export-tar' in arguments: remote_path,
borgmatic.actions.export_tar.run_export_tar( )
repository, elif action_name == 'export-tar':
storage, borgmatic.actions.export_tar.run_export_tar(
local_borg_version, repository,
arguments['export-tar'], storage,
global_arguments, local_borg_version,
local_path, action_arguments,
remote_path, global_arguments,
) local_path,
if 'mount' in arguments: remote_path,
borgmatic.actions.mount.run_mount( )
repository, storage, local_borg_version, arguments['mount'], local_path, remote_path, elif action_name == 'mount':
) borgmatic.actions.mount.run_mount(
if 'restore' in arguments: repository,
borgmatic.actions.restore.run_restore( storage,
repository, local_borg_version,
location, arguments['mount'],
storage, local_path,
hooks, remote_path,
local_borg_version, )
arguments['restore'], elif action_name == 'restore':
global_arguments, borgmatic.actions.restore.run_restore(
local_path, repository,
remote_path, location,
) storage,
if 'rlist' in arguments: hooks,
yield from borgmatic.actions.rlist.run_rlist( local_borg_version,
repository, storage, local_borg_version, arguments['rlist'], local_path, remote_path, action_arguments,
) global_arguments,
if 'list' in arguments: local_path,
yield from borgmatic.actions.list.run_list( remote_path,
repository, storage, local_borg_version, arguments['list'], local_path, remote_path, )
) elif action_name == 'rlist':
if 'rinfo' in arguments: yield from borgmatic.actions.rlist.run_rlist(
yield from borgmatic.actions.rinfo.run_rinfo( repository, storage, local_borg_version, action_arguments, local_path, remote_path,
repository, storage, local_borg_version, arguments['rinfo'], local_path, remote_path, )
) elif action_name == 'list':
if 'info' in arguments: yield from borgmatic.actions.list.run_list(
yield from borgmatic.actions.info.run_info( repository, storage, local_borg_version, action_arguments, local_path, remote_path,
repository, storage, local_borg_version, arguments['info'], local_path, remote_path, )
) elif action_name == 'rinfo':
if 'break-lock' in arguments: yield from borgmatic.actions.rinfo.run_rinfo(
borgmatic.actions.break_lock.run_break_lock( repository, storage, local_borg_version, action_arguments, local_path, remote_path,
repository, )
storage, elif action_name == 'info':
local_borg_version, yield from borgmatic.actions.info.run_info(
arguments['break-lock'], repository, storage, local_borg_version, action_arguments, local_path, remote_path,
local_path, )
remote_path, elif action_name == 'break-lock':
) borgmatic.actions.break_lock.run_break_lock(
if 'borg' in arguments: repository,
borgmatic.actions.borg.run_borg( storage,
repository, storage, local_borg_version, arguments['borg'], local_path, remote_path, 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( command.execute_hook(
hooks.get('after_actions'), hooks.get('after_actions'),

View file

@ -202,6 +202,12 @@ properties:
path prevents "borgmatic restore" from finding any database path prevents "borgmatic restore" from finding any database
dumps created before the change. Defaults to ~/.borgmatic dumps created before the change. Defaults to ~/.borgmatic
example: /tmp/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: storage:
type: object type: object
description: | description: |
@ -315,6 +321,12 @@ properties:
Path for Borg cache files. Defaults to Path for Borg cache files. Defaults to
$borg_base_directory/.cache/borg $borg_base_directory/.cache/borg
example: /path/to/base/cache 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: borg_security_directory:
type: string type: string
description: | description: |
@ -369,6 +381,11 @@ properties:
description: | description: |
Extra command-line options to pass to "borg init". Extra command-line options to pass to "borg init".
example: "--extra-option" example: "--extra-option"
create:
type: string
description: |
Extra command-line options to pass to "borg create".
example: "--extra-option"
prune: prune:
type: string type: string
description: | description: |
@ -379,11 +396,6 @@ properties:
description: | description: |
Extra command-line options to pass to "borg compact". Extra command-line options to pass to "borg compact".
example: "--extra-option" example: "--extra-option"
create:
type: string
description: |
Extra command-line options to pass to "borg create".
example: "--extra-option"
check: check:
type: string type: string
description: | description: |
@ -663,11 +675,11 @@ properties:
type: string type: string
description: | description: |
List of one or more shell commands or scripts to execute List of one or more shell commands or scripts to execute
when an exception occurs during a "prune", "compact", when an exception occurs during a "create", "prune",
"create", or "check" action or an associated before/after "compact", or "check" action or an associated before/after
hook. hook.
example: example:
- echo "Error during prune/compact/create/check." - echo "Error during create/prune/compact/check."
before_everything: before_everything:
type: array type: array
items: items:
@ -1168,7 +1180,7 @@ properties:
type: string type: string
description: | description: |
Healthchecks ping URL or UUID to notify when a 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 example: https://hc-ping.com/your-uuid-here
verify_tls: verify_tls:
type: boolean type: boolean
@ -1180,7 +1192,8 @@ properties:
type: boolean type: boolean
description: | description: |
Send borgmatic logs to Healthchecks as part the Send borgmatic logs to Healthchecks as part the
"finish" state. Defaults to true. "finish", "fail", and "log" states. Defaults to
true.
example: false example: false
ping_body_limit: ping_body_limit:
type: integer type: integer
@ -1199,10 +1212,11 @@ properties:
- start - start
- finish - finish
- fail - fail
- log
uniqueItems: true uniqueItems: true
description: | description: |
List of one or more monitoring states to ping for: 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. pinging for all states.
example: example:
- finish - finish

View file

@ -189,5 +189,5 @@ def guard_single_repository_selected(repository, configurations):
if count != 1: if count != 1:
raise ValueError( raise ValueError(
'Can\'t determine which repository to use. Use --repository to disambiguate' "Can't determine which repository to use. Use --repository to disambiguate"
) )

View file

@ -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 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. 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 '' dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
formatted_state = '/{}/'.format(MONITOR_STATE_TO_CRONHUB[state]) formatted_state = '/{}/'.format(MONITOR_STATE_TO_CRONHUB[state])
ping_url = ( ping_url = (

View file

@ -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 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. 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 '' dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
ping_url = '{}/{}'.format(hook_config['ping_url'], MONITOR_STATE_TO_CRONITOR[state]) ping_url = '{}/{}'.format(hook_config['ping_url'], MONITOR_STATE_TO_CRONITOR[state])

View file

@ -10,6 +10,7 @@ MONITOR_STATE_TO_HEALTHCHECKS = {
monitor.State.START: 'start', monitor.State.START: 'start',
monitor.State.FINISH: None, # Healthchecks doesn't append to the URL for the finished state. monitor.State.FINISH: None, # Healthchecks doesn't append to the URL for the finished state.
monitor.State.FAIL: 'fail', monitor.State.FAIL: 'fail',
monitor.State.LOG: 'log',
} }
PAYLOAD_TRUNCATION_INDICATOR = '...\n' 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)) 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() payload = format_buffered_logs_for_payload()
else: else:
payload = '' payload = ''

View file

@ -7,3 +7,4 @@ class State(Enum):
START = 1 START = 1
FINISH = 2 FINISH = 2
FAIL = 3 FAIL = 3
LOG = 4

View file

@ -2,16 +2,8 @@ import logging
import requests import requests
from borgmatic.hooks import monitor
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
MONITOR_STATE_TO_NTFY = {
monitor.State.START: None,
monitor.State.FINISH: None,
monitor.State.FAIL: None,
}
def initialize_monitor( def initialize_monitor(
ping_url, config_filename, monitoring_log_level, dry_run ping_url, config_filename, monitoring_log_level, dry_run

View file

@ -4,7 +4,7 @@ COPY . /app
RUN apk add --no-cache py3-pip py3-ruamel.yaml py3-ruamel.yaml.clib 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 pip install --no-cache /app && generate-borgmatic-config && chmod +r /etc/borgmatic/config.yaml
RUN borgmatic --help > /command-line.txt \ 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 \ echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \
&& borgmatic "$action" --help >> /command-line.txt; done && borgmatic "$action" --help >> /command-line.txt; done

View file

@ -316,7 +316,10 @@ user and you're extracting to `/tmp`, then the dump will be in
`/tmp/root/.borgmatic`. `/tmp/root/.borgmatic`.
After extraction, you can manually restore the dump file using native database 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 ## Preparation and cleanup hooks

View file

@ -9,37 +9,47 @@ eleventyNavigation:
Borg itself is great for efficiently de-duplicating data across successive Borg itself is great for efficiently de-duplicating data across successive
backup archives, even when dealing with very large repositories. But you may backup archives, even when dealing with very large repositories. But you may
find that while borgmatic's default mode of `prune`, `compact`, `create`, and find that while borgmatic's default actions of `create`, `prune`, `compact`,
`check` works well on small repositories, it's not so great on larger ones. and `check` works well on small repositories, it's not so great on larger
That's because running the default pruning, compact, and consistency checks ones. That's because running the default pruning, compact, and consistency
take a long time on large repositories. checks take a long time on large repositories.
<span class="minilink minilink-addedin">Prior to version 1.7.9</span> The
default action ordering was `prune`, `compact`, `create`, and `check`.
### A la carte actions ### A la carte actions
If you find yourself in this situation, you have some options. First, you can If you find yourself wanting to customize the actions, you have some options.
run borgmatic's `prune`, `compact`, `create`, or `check` actions separately. First, you can run borgmatic's `prune`, `compact`, `create`, or `check`
For instance, the following optional actions are available: actions separately. For instance, the following optional actions are
available (among others):
```bash ```bash
borgmatic create
borgmatic prune borgmatic prune
borgmatic compact borgmatic compact
borgmatic create
borgmatic check borgmatic check
``` ```
You can run with only one of these actions provided, or you can mix and match You can run borgmatic with only one of these actions provided, or you can mix
any number of them in a single borgmatic run. This supports approaches like and match any number of them in a single borgmatic run. This supports
skipping certain actions while running others. For instance, this skips approaches like skipping certain actions while running others. For instance,
`prune` and `compact` and only runs `create` and `check`: this skips `prune` and `compact` and only runs `create` and `check`:
```bash ```bash
borgmatic create check borgmatic create check
``` ```
Or, you can make backups with `create` on a frequent schedule (e.g. with <span class="minilink minilink-addedin">New in version 1.7.9</span> borgmatic
`borgmatic create` called from one cron job), while only running expensive now respects your specified command-line action order, running actions in the
consistency checks with `check` on a much less frequent basis (e.g. with order you specify. In previous versions, borgmatic ran your specified actions
`borgmatic check` called from a separate cron job). 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 ### 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 Another option is to customize your consistency checks. By default, if you
omit consistency checks from configuration, borgmatic runs full-repository omit consistency checks from configuration, borgmatic runs full-repository
checks (`repository`) and per-archive checks (`archives`) within each checks (`repository`) and per-archive checks (`archives`) within each
repository, no more than once a month. This is equivalent to what `borg check` repository. (Although see below about check frequency.) This is equivalent to
does if run without options. what `borg check` does if run without options.
But if you find that archive checks are too slow, for example, you can 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 configure borgmatic to run repository checks only. Configure this in the
@ -60,8 +70,9 @@ consistency:
- name: repository - name: repository
``` ```
<span class="minilink minilink-addedin">Prior to version 1.6.2</span> `checks` <span class="minilink minilink-addedin">Prior to version 1.6.2</span> The
was a plain list of strings without the `name:` part. For example: `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 ```yaml
consistency: consistency:
@ -102,8 +113,13 @@ consistency:
This tells borgmatic to run the `repository` consistency check at most once 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 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 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 days", "1 week", "2 months", etc.
means run this check every time checks run.
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 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 checks on the configured frequency. It compares that frequency with how long

View file

@ -26,7 +26,7 @@ make sure your changes work.
```bash ```bash
cd borgmatic/ cd borgmatic/
pip3 install --editable --user . pip3 install --user --editable .
``` ```
Note that this will typically install the borgmatic commands into Note that this will typically install the borgmatic commands into

View file

@ -20,15 +20,15 @@ borgmatic rlist
That should yield output looking something like: That should yield output looking something like:
```text ```text
host-2019-01-01T04:05:06.070809 Tue, 2019-01-01 04:05:06 [...] host-2023-01-01T04:05:06.070809 Tue, 2023-01-01 04:05:06 [...]
host-2019-01-02T04:06:07.080910 Wed, 2019-01-02 04:06:07 [...] 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 Assuming that you want to extract the archive with the most up-to-date files
and therefore the latest timestamp, run a command like: and therefore the latest timestamp, run a command like:
```bash ```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!) (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: the repository path containing the archive to extract. Here's an example:
```bash ```bash
borgmatic extract --repository repo.borg --archive host-2019-... borgmatic extract --repository repo.borg --archive host-2023-...
``` ```
## Extract particular files ## Extract particular files
@ -74,6 +74,13 @@ run the `extract` command above, borgmatic will extract `/var/path/1` and
`/var/path/2`. `/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 ## Extract to a particular destination
By default, borgmatic extracts files into the current directory. To instead By default, borgmatic extracts files into the current directory. To instead

View file

@ -91,6 +91,19 @@ example, to search only the last five archives:
borgmatic list --find foo.txt --last 5 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 ## Logging

View file

@ -83,7 +83,7 @@ tests](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/).
## Error hooks ## 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 borgmatic can run configurable shell commands to fire off custom error
notifications or take other actions, so you can get alerted as soon as notifications or take other actions, so you can get alerted as soon as
something goes wrong. Here's a not-so-useful example: 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 * `output`: output of the command that failed (may be blank if an error
occurred without running a command) occurred without running a command)
Note that borgmatic runs the `on_error` hooks only for `prune`, `compact`, Note that borgmatic runs the `on_error` hooks only for `create`, `prune`,
`create`, or `check` actions or hooks in which an error occurs, and not other `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 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 `before_everything` or `after_everything` hook. For more about hooks, see the
[borgmatic hooks [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 <a backup begins, ends, or errors. Specifically, after the <a
href="https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/">`before_backup` href="https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/">`before_backup`
hooks</a> run, borgmatic lets Healthchecks know that it has started if any of hooks</a> 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 Then, if the actions complete successfully, borgmatic notifies Healthchecks of
the success after the `after_backup` hooks run, and includes borgmatic logs in 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 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 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`, itself. But the logs are only included for errors that occur when a `create`,
`compact`, `create`, or `check` action is run. `prune`, `compact`, or `check` action is run.
You can customize the verbosity of the logs that are sent to Healthchecks with You can customize the verbosity of the logs that are sent to Healthchecks with
borgmatic's `--monitoring-verbosity` flag. The `--list` and `--stats` flags borgmatic's `--monitoring-verbosity` flag. The `--list` and `--stats` flags

View file

@ -94,6 +94,7 @@ installing borgmatic:
* [openSUSE](https://software.opensuse.org/package/borgmatic) * [openSUSE](https://software.opensuse.org/package/borgmatic)
* [macOS (via Homebrew)](https://formulae.brew.sh/formula/borgmatic) * [macOS (via Homebrew)](https://formulae.brew.sh/formula/borgmatic)
* [macOS (via MacPorts)](https://ports.macports.org/port/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) * [Ansible role](https://github.com/borgbase/ansible-role-borgbackup)
* [virtualenv](https://virtualenv.pypa.io/en/stable/) * [virtualenv](https://virtualenv.pypa.io/en/stable/)

View file

@ -10,6 +10,8 @@ filterwarnings =
[flake8] [flake8]
ignore = E501,W503 ignore = E501,W503
exclude = *.*/* exclude = *.*/*
multiline-quotes = '''
docstring-quotes = '''
[tool:isort] [tool:isort]
force_single_line = False force_single_line = False

View file

@ -1,6 +1,6 @@
from setuptools import find_packages, setup from setuptools import find_packages, setup
VERSION = '1.7.9.dev0' VERSION = '1.7.10.dev0'
setup( setup(

View file

@ -5,6 +5,7 @@ click==7.1.2; python_version >= '3.8'
colorama==0.4.4 colorama==0.4.4
coverage==5.3 coverage==5.3
flake8==4.0.1 flake8==4.0.1
flake8-quotes==3.3.2
flexmock==0.10.4 flexmock==0.10.4
isort==5.9.1 isort==5.9.1
mccabe==0.6.1 mccabe==0.6.1

View file

@ -254,13 +254,6 @@ def test_parse_arguments_allows_init_and_create():
module.parse_arguments('--config', 'myconfig', 'init', '--encryption', 'repokey', '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(): def test_parse_arguments_allows_repository_with_extract():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])

View file

@ -3,15 +3,78 @@ from flexmock import flexmock
from borgmatic.actions import check as module 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.logger).answer = lambda message: None
flexmock(module.borgmatic.config.checks).should_receive( flexmock(module.borgmatic.config.checks).should_receive(
'repository_enabled_for_checks' 'repository_enabled_for_checks'
).and_return(True) ).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) flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
check_arguments = flexmock( 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) global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)

View file

@ -3,13 +3,70 @@ from flexmock import flexmock
from borgmatic.actions import compact as module 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.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True) 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) flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
compact_arguments = flexmock( 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) global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)

View file

@ -3,16 +3,87 @@ from flexmock import flexmock
from borgmatic.actions import create as module 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.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.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').and_return({})
flexmock(module.borgmatic.hooks.dispatch).should_receive( flexmock(module.borgmatic.hooks.dispatch).should_receive(
'call_hooks_even_if_unconfigured' 'call_hooks_even_if_unconfigured'
).and_return({}) ).and_return({})
create_arguments = flexmock( 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) global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)

View file

@ -3,11 +3,62 @@ from flexmock import flexmock
from borgmatic.actions import prune as module 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.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) 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) global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
module.run_prune( module.run_prune(

View file

@ -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(): def test_make_exclude_flags_includes_exclude_from_filenames_when_in_config():
exclude_flags = module.make_exclude_flags( exclude_flags = module.make_exclude_flags(
location_config={'exclude_from': ['excludes', 'other']} 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}',) (f'repo::{DEFAULT_ARCHIVE_NAME}',)
) )
flexmock(module.environment).should_receive('make_environment') 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 = ( create_command = (
'borg', 'borg',
'create', 'create',
@ -2530,3 +2529,27 @@ def test_create_archive_with_stream_processes_calls_borg_with_processes_and_read
local_borg_version='1.2.3', local_borg_version='1.2.3',
stream_processes=processes, 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',
)

View file

@ -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}) environment = module.make_environment({'relocated_repo_access_is_ok': True})
assert environment.get('BORG_RELOCATED_REPO_ACCESS_IS_OK') == 'yes' 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'

View file

@ -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(): def test_extract_archive_calls_borg_with_progress_parameter():
flexmock(module.os.path).should_receive('abspath').and_return('repo') flexmock(module.os.path).should_receive('abspath').and_return('repo')
flexmock(module.environment).should_receive('make_environment') flexmock(module.environment).should_receive('make_environment')

View file

@ -1,3 +1,5 @@
import collections
from flexmock import flexmock from flexmock import flexmock
from borgmatic.commands import arguments as module from borgmatic.commands import arguments as module
@ -70,6 +72,26 @@ def test_parse_subparser_arguments_consumes_multiple_subparser_arguments():
assert remaining_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(): def test_parse_subparser_arguments_applies_default_subparsers():
prune_namespace = flexmock() prune_namespace = flexmock()
compact_namespace = flexmock() compact_namespace = flexmock()

View file

@ -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.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.dispatch).should_receive('call_hooks').and_raise(OSError).and_return( flexmock(module.dispatch).should_receive('call_hooks').and_raise(OSError).and_return(
None None
).and_return(None) ).and_return(None).and_return(None)
expected_results = [flexmock()] expected_results = [flexmock()]
flexmock(module).should_receive('log_error_records').and_return(expected_results) flexmock(module).should_receive('log_error_records').and_return(expected_results)
flexmock(module).should_receive('run_actions').never() flexmock(module).should_receive('run_actions').never()
@ -99,7 +99,7 @@ def test_run_configuration_bails_for_actions_soft_failure():
assert results == [] 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).should_receive('verbosity_to_log_level').and_return(logging.INFO)
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock()) flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.dispatch).should_receive('call_hooks').and_return(None).and_return( 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 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(): def test_run_configuration_bails_for_monitor_finish_soft_failure():
flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO) 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.borg_version).should_receive('local_borg_version').and_return(flexmock())
error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again') error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
flexmock(module.dispatch).should_receive('call_hooks').and_return(None).and_return( flexmock(module.dispatch).should_receive('call_hooks').and_return(None).and_return(
None None
).and_raise(error) ).and_raise(None).and_raise(error)
flexmock(module).should_receive('log_error_records').never() flexmock(module).should_receive('log_error_records').never()
flexmock(module).should_receive('run_actions').and_return([]) flexmock(module).should_receive('run_actions').and_return([])
flexmock(module.command).should_receive('considered_soft_failure').and_return(True) 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(): def test_run_actions_runs_prune():
flexmock(module).should_receive('add_custom_log_levels') flexmock(module).should_receive('add_custom_log_levels')
flexmock(module.command).should_receive('execute_hook') 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(): def test_run_actions_runs_check_when_repository_enabled_for_checks():
flexmock(module).should_receive('add_custom_log_levels') flexmock(module).should_receive('add_custom_log_levels')
flexmock(module.command).should_receive('execute_hook') 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(): def test_load_configurations_collects_parsed_configurations_and_logs():
configuration = flexmock() configuration = flexmock()
other_configuration = flexmock() other_configuration = flexmock()

View file

@ -102,3 +102,11 @@ def test_ping_monitor_with_other_error_logs_warning():
monitoring_log_level=1, monitoring_log_level=1,
dry_run=False, 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,
)

View file

@ -87,3 +87,11 @@ def test_ping_monitor_with_other_error_logs_warning():
monitoring_log_level=1, monitoring_log_level=1,
dry_run=False, 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,
)

View file

@ -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(): def test_ping_monitor_with_ping_uuid_hits_corresponding_url():
hook_config = {'ping_url': 'abcd-efgh-ijkl-mnop'} hook_config = {'ping_url': 'abcd-efgh-ijkl-mnop'}
payload = 'data' payload = 'data'

View file

@ -72,7 +72,7 @@ def test_dump_databases_runs_mongodump_with_username_and_password():
'name': 'foo', 'name': 'foo',
'username': 'mongo', 'username': 'mongo',
'password': 'trustsome1', 'password': 'trustsome1',
'authentication_database': "admin", 'authentication_database': 'admin',
} }
] ]
process = flexmock() process = flexmock()

View file

@ -2,6 +2,7 @@ from enum import Enum
from flexmock import flexmock from flexmock import flexmock
import borgmatic.hooks.monitor
from borgmatic.hooks import ntfy as module from borgmatic.hooks import ntfy as module
default_base_url = 'https://ntfy.sh' 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} hook_config = {'topic': topic}
flexmock(module.requests).should_receive('post').with_args( flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}', 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, auth=None,
).and_return(flexmock(ok=True)).once() ).and_return(flexmock(ok=True)).once()
module.ping_monitor( 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( flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}', 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'), auth=module.requests.auth.HTTPBasicAuth('testuser', 'fakepassword'),
).and_return(flexmock(ok=True)).once() ).and_return(flexmock(ok=True)).once()
module.ping_monitor( 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'} hook_config = {'topic': topic, 'password': 'fakepassword'}
flexmock(module.requests).should_receive('post').with_args( flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}', 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, auth=None,
).and_return(flexmock(ok=True)).once() ).and_return(flexmock(ok=True)).once()
flexmock(module.logger).should_receive('warning').once() flexmock(module.logger).should_receive('warning').once()
module.ping_monitor( 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'} hook_config = {'topic': topic, 'username': 'testuser'}
flexmock(module.requests).should_receive('post').with_args( flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}', 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, auth=None,
).and_return(flexmock(ok=True)).once() ).and_return(flexmock(ok=True)).once()
flexmock(module.logger).should_receive('warning').once() flexmock(module.logger).should_receive('warning').once()
module.ping_monitor( 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( module.ping_monitor(
hook_config, hook_config,
'config.yaml', 'config.yaml',
module.monitor.State.START, borgmatic.hooks.monitor.State.START,
monitoring_log_level=1, monitoring_log_level=1,
dry_run=False, dry_run=False,
) )
@ -111,7 +128,7 @@ def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_finish():
module.ping_monitor( module.ping_monitor(
hook_config, hook_config,
'config.yaml', 'config.yaml',
module.monitor.State.FINISH, borgmatic.hooks.monitor.State.FINISH,
monitoring_log_level=1, monitoring_log_level=1,
dry_run=False, 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} hook_config = {'topic': topic, 'server': custom_base_url}
flexmock(module.requests).should_receive('post').with_args( flexmock(module.requests).should_receive('post').with_args(
f'{custom_base_url}/{topic}', 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, auth=None,
).and_return(flexmock(ok=True)).once() ).and_return(flexmock(ok=True)).once()
module.ping_monitor( 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() flexmock(module.requests).should_receive('post').never()
module.ping_monitor( 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() ).and_return(flexmock(ok=True)).once()
module.ping_monitor( 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']} hook_config = {'topic': topic, 'states': ['start', 'fail']}
flexmock(module.requests).should_receive('post').with_args( flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}', 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, auth=None,
).and_return(flexmock(ok=True)).once() ).and_return(flexmock(ok=True)).once()
module.ping_monitor( module.ping_monitor(
hook_config, hook_config,
'config.yaml', 'config.yaml',
module.monitor.State.START, borgmatic.hooks.monitor.State.START,
monitoring_log_level=1, monitoring_log_level=1,
dry_run=False, dry_run=False,
) )
@ -171,7 +200,7 @@ def test_ping_monitor_with_connection_error_logs_warning():
hook_config = {'topic': topic} hook_config = {'topic': topic}
flexmock(module.requests).should_receive('post').with_args( flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}', 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, auth=None,
).and_raise(module.requests.exceptions.ConnectionError) ).and_raise(module.requests.exceptions.ConnectionError)
flexmock(module.logger).should_receive('warning').once() flexmock(module.logger).should_receive('warning').once()
@ -179,7 +208,7 @@ def test_ping_monitor_with_connection_error_logs_warning():
module.ping_monitor( module.ping_monitor(
hook_config, hook_config,
'config.yaml', 'config.yaml',
module.monitor.State.FAIL, borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1, monitoring_log_level=1,
dry_run=False, dry_run=False,
) )
@ -193,7 +222,7 @@ def test_ping_monitor_with_other_error_logs_warning():
) )
flexmock(module.requests).should_receive('post').with_args( flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}', 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, auth=None,
).and_return(response) ).and_return(response)
flexmock(module.logger).should_receive('warning').once() flexmock(module.logger).should_receive('warning').once()
@ -201,7 +230,7 @@ def test_ping_monitor_with_other_error_logs_warning():
module.ping_monitor( module.ping_monitor(
hook_config, hook_config,
'config.yaml', 'config.yaml',
module.monitor.State.FAIL, borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1, monitoring_log_level=1,
dry_run=False, dry_run=False,
) )

View file

@ -1,5 +1,5 @@
[tox] [tox]
envlist = py37,py38,py39,py310 envlist = py37,py38,py39,py310,py311
skip_missing_interpreters = True skip_missing_interpreters = True
skipsdist = True skipsdist = True
minversion = 3.14.1 minversion = 3.14.1
@ -13,7 +13,7 @@ whitelist_externals =
passenv = COVERAGE_FILE passenv = COVERAGE_FILE
commands = commands =
pytest {posargs} pytest {posargs}
py38,py39,py310: black --check . py38,py39,py310,py311: black --check .
isort --check-only --settings-path setup.cfg . isort --check-only --settings-path setup.cfg .
flake8 borgmatic tests flake8 borgmatic tests