Compare commits
54 commits
1.9.14
...
23efbb8df3
| Author | SHA1 | Date | |
|---|---|---|---|
| 23efbb8df3 | |||
| 9e694e4df9 | |||
| 76f7c53a1c | |||
| 532a97623c | |||
| e1fdfe4c2f | |||
| 83a56a3fef | |||
|
|
4bca7bb198 | ||
| 6a470be924 | |||
| d651813601 | |||
| 524ec6b3cb | |||
| 7904ffb641 | |||
| cd5ba81748 | |||
| 514ade6609 | |||
| 201469e2c2 | |||
| 9ac2a2e286 | |||
|
|
a16d138afc | ||
|
|
81a3a99578 | ||
| 587d31de7c | |||
|
|
8aaa5ba8a6 | ||
|
|
5525b467ef | ||
| c2409d9968 | |||
| 624a7de622 | |||
| c926f0bd5d | |||
| 1d5713c4c5 | |||
| f9612cc685 | |||
| 5742a1a2d9 | |||
|
|
c84815bfb0 | ||
| 1c92d84e09 | |||
| 1d94fb501f | |||
|
|
1b4c94ad1e | ||
| 901e668c76 | |||
| bcb224a243 | |||
| 6b6e1e0336 | |||
| f5c9bc4fa9 | |||
| cdd0e6f052 | |||
| 7bdbadbac2 | |||
| d3413e0907 | |||
| 8a20ee7304 | |||
| 325f53c286 | |||
| b4d24798bf | |||
| 7965eb9de3 | |||
| 8817364e6d | |||
| 965740c778 | |||
| 2a0319f02f | |||
| 9941d7dc57 | |||
| ec88bb2e9c | |||
| b52339652f | |||
| 4fd22b2df0 | |||
| 5ab766b51c | |||
| 45c114973c | |||
| 6a96a78cf1 | |||
| e06c6740f2 | |||
| 10bd1c7b41 | |||
| d4f48a3a9e |
55 changed files with 3664 additions and 1204 deletions
19
NEWS
19
NEWS
|
|
@ -1,3 +1,22 @@
|
|||
2.0.0.dev0
|
||||
* #345: Add a "key import" action to import a repository key from backup.
|
||||
* #422: Add home directory expansion to file-based and KeePassXC credential hooks.
|
||||
* #790, #821: Deprecate all "before_*", "after_*" and "on_error" command hooks in favor of more
|
||||
flexible "commands:". See the documentation for more information:
|
||||
https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/
|
||||
* #790: BREAKING: For both new and deprecated command hooks, run a configured "after" hook even if
|
||||
an error occurs first. This allows you to perform cleanup steps that correspond to "before"
|
||||
preparation commands—even when something goes wrong.
|
||||
* #790: BREAKING: Run all command hooks (both new and deprecated) respecting the
|
||||
"working_directory" option if configured, meaning that hook commands are run in that directory.
|
||||
* #836: Add a custom command option for the SQLite hook.
|
||||
* #837: Add custom command options for the MongoDB hook.
|
||||
* #1010: When using Borg 2, don't pass the "--stats" flag to "borg prune".
|
||||
* #1020: Document a database use case involving a temporary database client container:
|
||||
https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#containers
|
||||
* #1037: Fix an error with the "extract" action when both a remote repository and a
|
||||
"working_directory" are used.
|
||||
|
||||
1.9.14
|
||||
* #409: With the PagerDuty monitoring hook, send borgmatic logs to PagerDuty so they show up in the
|
||||
incident UI. See the documentation for more information:
|
||||
|
|
|
|||
|
|
@ -682,7 +682,6 @@ def run_check(
|
|||
config_filename,
|
||||
repository,
|
||||
config,
|
||||
hook_context,
|
||||
local_borg_version,
|
||||
check_arguments,
|
||||
global_arguments,
|
||||
|
|
@ -699,15 +698,6 @@ def run_check(
|
|||
):
|
||||
return
|
||||
|
||||
borgmatic.hooks.command.execute_hook(
|
||||
config.get('before_check'),
|
||||
config.get('umask'),
|
||||
config_filename,
|
||||
'pre-check',
|
||||
global_arguments.dry_run,
|
||||
**hook_context,
|
||||
)
|
||||
|
||||
logger.info('Running consistency checks')
|
||||
|
||||
repository_id = borgmatic.borg.check.get_repository_id(
|
||||
|
|
@ -772,12 +762,3 @@ def run_check(
|
|||
borgmatic_runtime_directory,
|
||||
)
|
||||
write_check_time(make_check_time_path(config, repository_id, 'spot'))
|
||||
|
||||
borgmatic.hooks.command.execute_hook(
|
||||
config.get('after_check'),
|
||||
config.get('umask'),
|
||||
config_filename,
|
||||
'post-check',
|
||||
global_arguments.dry_run,
|
||||
**hook_context,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ def run_compact(
|
|||
config_filename,
|
||||
repository,
|
||||
config,
|
||||
hook_context,
|
||||
local_borg_version,
|
||||
compact_arguments,
|
||||
global_arguments,
|
||||
|
|
@ -28,14 +27,6 @@ def run_compact(
|
|||
):
|
||||
return
|
||||
|
||||
borgmatic.hooks.command.execute_hook(
|
||||
config.get('before_compact'),
|
||||
config.get('umask'),
|
||||
config_filename,
|
||||
'pre-compact',
|
||||
global_arguments.dry_run,
|
||||
**hook_context,
|
||||
)
|
||||
if borgmatic.borg.feature.available(borgmatic.borg.feature.Feature.COMPACT, local_borg_version):
|
||||
logger.info(f'Compacting segments{dry_run_label}')
|
||||
borgmatic.borg.compact.compact_segments(
|
||||
|
|
@ -52,12 +43,3 @@ def run_compact(
|
|||
)
|
||||
else: # pragma: nocover
|
||||
logger.info('Skipping compact (only available/needed in Borg 1.2+)')
|
||||
|
||||
borgmatic.hooks.command.execute_hook(
|
||||
config.get('after_compact'),
|
||||
config.get('umask'),
|
||||
config_filename,
|
||||
'post-compact',
|
||||
global_arguments.dry_run,
|
||||
**hook_context,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -272,7 +272,6 @@ def run_create(
|
|||
repository,
|
||||
config,
|
||||
config_paths,
|
||||
hook_context,
|
||||
local_borg_version,
|
||||
create_arguments,
|
||||
global_arguments,
|
||||
|
|
@ -290,15 +289,6 @@ def run_create(
|
|||
):
|
||||
return
|
||||
|
||||
borgmatic.hooks.command.execute_hook(
|
||||
config.get('before_backup'),
|
||||
config.get('umask'),
|
||||
config_filename,
|
||||
'pre-backup',
|
||||
global_arguments.dry_run,
|
||||
**hook_context,
|
||||
)
|
||||
|
||||
logger.info(f'Creating archive{dry_run_label}')
|
||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
||||
|
||||
|
|
@ -354,12 +344,3 @@ def run_create(
|
|||
borgmatic_runtime_directory,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
|
||||
borgmatic.hooks.command.execute_hook(
|
||||
config.get('after_backup'),
|
||||
config.get('umask'),
|
||||
config_filename,
|
||||
'post-backup',
|
||||
global_arguments.dry_run,
|
||||
**hook_context,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ def run_extract(
|
|||
config_filename,
|
||||
repository,
|
||||
config,
|
||||
hook_context,
|
||||
local_borg_version,
|
||||
extract_arguments,
|
||||
global_arguments,
|
||||
|
|
@ -22,14 +21,6 @@ def run_extract(
|
|||
'''
|
||||
Run the "extract" action for the given repository.
|
||||
'''
|
||||
borgmatic.hooks.command.execute_hook(
|
||||
config.get('before_extract'),
|
||||
config.get('umask'),
|
||||
config_filename,
|
||||
'pre-extract',
|
||||
global_arguments.dry_run,
|
||||
**hook_context,
|
||||
)
|
||||
if extract_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||
repository, extract_arguments.repository
|
||||
):
|
||||
|
|
@ -56,11 +47,3 @@ def run_extract(
|
|||
strip_components=extract_arguments.strip_components,
|
||||
progress=extract_arguments.progress,
|
||||
)
|
||||
borgmatic.hooks.command.execute_hook(
|
||||
config.get('after_extract'),
|
||||
config.get('umask'),
|
||||
config_filename,
|
||||
'post-extract',
|
||||
global_arguments.dry_run,
|
||||
**hook_context,
|
||||
)
|
||||
|
|
|
|||
33
borgmatic/actions/import_key.py
Normal file
33
borgmatic/actions/import_key.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import logging
|
||||
|
||||
import borgmatic.borg.import_key
|
||||
import borgmatic.config.validate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def run_import_key(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
import_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
):
|
||||
'''
|
||||
Run the "key import" action for the given repository.
|
||||
'''
|
||||
if import_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||
repository, import_arguments.repository
|
||||
):
|
||||
logger.info('Importing repository key')
|
||||
borgmatic.borg.import_key.import_key(
|
||||
repository['path'],
|
||||
config,
|
||||
local_borg_version,
|
||||
import_arguments,
|
||||
global_arguments,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
)
|
||||
|
|
@ -11,7 +11,6 @@ def run_prune(
|
|||
config_filename,
|
||||
repository,
|
||||
config,
|
||||
hook_context,
|
||||
local_borg_version,
|
||||
prune_arguments,
|
||||
global_arguments,
|
||||
|
|
@ -27,14 +26,6 @@ def run_prune(
|
|||
):
|
||||
return
|
||||
|
||||
borgmatic.hooks.command.execute_hook(
|
||||
config.get('before_prune'),
|
||||
config.get('umask'),
|
||||
config_filename,
|
||||
'pre-prune',
|
||||
global_arguments.dry_run,
|
||||
**hook_context,
|
||||
)
|
||||
logger.info(f'Pruning archives{dry_run_label}')
|
||||
borgmatic.borg.prune.prune_archives(
|
||||
global_arguments.dry_run,
|
||||
|
|
@ -46,11 +37,3 @@ def run_prune(
|
|||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
)
|
||||
borgmatic.hooks.command.execute_hook(
|
||||
config.get('after_prune'),
|
||||
config.get('umask'),
|
||||
config_filename,
|
||||
'post-prune',
|
||||
global_arguments.dry_run,
|
||||
**hook_context,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -134,9 +134,7 @@ def extract_archive(
|
|||
# Make the repository path absolute so the destination directory used below via changing
|
||||
# the working directory doesn't prevent Borg from finding the repo. But also apply the
|
||||
# user's configured working directory (if any) to the repo path.
|
||||
borgmatic.config.validate.normalize_repository_path(
|
||||
os.path.join(working_directory or '', repository)
|
||||
),
|
||||
borgmatic.config.validate.normalize_repository_path(repository, working_directory),
|
||||
archive,
|
||||
local_borg_version,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ class Feature(Enum):
|
|||
MATCH_ARCHIVES = 11
|
||||
EXCLUDED_FILES_MINUS = 12
|
||||
ARCHIVE_SERIES = 13
|
||||
NO_PRUNE_STATS = 14
|
||||
|
||||
|
||||
FEATURE_TO_MINIMUM_BORG_VERSION = {
|
||||
|
|
@ -33,6 +34,7 @@ FEATURE_TO_MINIMUM_BORG_VERSION = {
|
|||
Feature.MATCH_ARCHIVES: parse('2.0.0b3'), # borg --match-archives
|
||||
Feature.EXCLUDED_FILES_MINUS: parse('2.0.0b5'), # --list --filter uses "-" for excludes
|
||||
Feature.ARCHIVE_SERIES: parse('2.0.0b11'), # identically named archives form a series
|
||||
Feature.NO_PRUNE_STATS: parse('2.0.0b10'), # prune --stats is not available
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
70
borgmatic/borg/import_key.py
Normal file
70
borgmatic/borg/import_key.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import logging
|
||||
import os
|
||||
|
||||
import borgmatic.config.paths
|
||||
import borgmatic.logger
|
||||
from borgmatic.borg import environment, flags
|
||||
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def import_key(
|
||||
repository_path,
|
||||
config,
|
||||
local_borg_version,
|
||||
import_arguments,
|
||||
global_arguments,
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
):
|
||||
'''
|
||||
Given a local or remote repository path, a configuration dict, the local Borg version, import
|
||||
arguments, and optional local and remote Borg paths, import the repository key from the
|
||||
path indicated in the import arguments.
|
||||
|
||||
If the path is empty or "-", then read the key from stdin.
|
||||
|
||||
Raise ValueError if the path is given and it does not exist.
|
||||
'''
|
||||
umask = config.get('umask', None)
|
||||
lock_wait = config.get('lock_wait', None)
|
||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
||||
|
||||
if import_arguments.path and import_arguments.path != '-':
|
||||
if not os.path.exists(os.path.join(working_directory or '', import_arguments.path)):
|
||||
raise ValueError(f'Path {import_arguments.path} does not exist. Aborting.')
|
||||
|
||||
input_file = None
|
||||
else:
|
||||
input_file = DO_NOT_CAPTURE
|
||||
|
||||
full_command = (
|
||||
(local_path, 'key', 'import')
|
||||
+ (('--remote-path', remote_path) if remote_path else ())
|
||||
+ (('--umask', str(umask)) if umask else ())
|
||||
+ (('--log-json',) if global_arguments.log_json else ())
|
||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ flags.make_flags('paper', import_arguments.paper)
|
||||
+ flags.make_repository_flags(
|
||||
repository_path,
|
||||
local_borg_version,
|
||||
)
|
||||
+ ((import_arguments.path,) if input_file is None else ())
|
||||
)
|
||||
|
||||
if global_arguments.dry_run:
|
||||
logger.info('Skipping key import (dry run)')
|
||||
return
|
||||
|
||||
execute_command(
|
||||
full_command,
|
||||
input_file=input_file,
|
||||
output_log_level=logging.INFO,
|
||||
environment=environment.make_environment(config),
|
||||
working_directory=working_directory,
|
||||
borg_local_path=local_path,
|
||||
borg_exit_codes=config.get('borg_exit_codes'),
|
||||
)
|
||||
|
|
@ -75,7 +75,13 @@ def prune_archives(
|
|||
+ (('--umask', str(umask)) if umask else ())
|
||||
+ (('--log-json',) if global_arguments.log_json else ())
|
||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||
+ (('--stats',) if prune_arguments.stats and not dry_run else ())
|
||||
+ (
|
||||
('--stats',)
|
||||
if prune_arguments.stats
|
||||
and not dry_run
|
||||
and not feature.available(feature.Feature.NO_PRUNE_STATS, local_borg_version)
|
||||
else ()
|
||||
)
|
||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||
+ flags.make_flags_from_arguments(
|
||||
prune_arguments,
|
||||
|
|
|
|||
|
|
@ -547,7 +547,7 @@ def make_parsers():
|
|||
dest='stats',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='Display statistics of the pruned archive',
|
||||
help='Display statistics of the pruned archive [Borg 1 only]',
|
||||
)
|
||||
prune_group.add_argument(
|
||||
'--list', dest='list_archives', action='store_true', help='List archives kept/pruned'
|
||||
|
|
@ -1479,6 +1479,31 @@ def make_parsers():
|
|||
'-h', '--help', action='help', help='Show this help message and exit'
|
||||
)
|
||||
|
||||
key_import_parser = key_parsers.add_parser(
|
||||
'import',
|
||||
help='Import a copy of the repository key from backup',
|
||||
description='Import a copy of the repository key from backup',
|
||||
add_help=False,
|
||||
)
|
||||
key_import_group = key_import_parser.add_argument_group('key import arguments')
|
||||
key_import_group.add_argument(
|
||||
'--paper',
|
||||
action='store_true',
|
||||
help='Import interactively from a backup done with --paper',
|
||||
)
|
||||
key_import_group.add_argument(
|
||||
'--repository',
|
||||
help='Path of repository to import the key from, defaults to the configured repository if there is only one, quoted globs supported',
|
||||
)
|
||||
key_import_group.add_argument(
|
||||
'--path',
|
||||
metavar='PATH',
|
||||
help='Path to import the key from backup, defaults to stdin',
|
||||
)
|
||||
key_import_group.add_argument(
|
||||
'-h', '--help', action='help', help='Show this help message and exit'
|
||||
)
|
||||
|
||||
key_change_passphrase_parser = key_parsers.add_parser(
|
||||
'change-passphrase',
|
||||
help='Change the passphrase protecting the repository key',
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import borgmatic.actions.delete
|
|||
import borgmatic.actions.export_key
|
||||
import borgmatic.actions.export_tar
|
||||
import borgmatic.actions.extract
|
||||
import borgmatic.actions.import_key
|
||||
import borgmatic.actions.info
|
||||
import borgmatic.actions.list
|
||||
import borgmatic.actions.mount
|
||||
|
|
@ -33,6 +34,7 @@ import borgmatic.actions.restore
|
|||
import borgmatic.actions.transfer
|
||||
import borgmatic.commands.completion.bash
|
||||
import borgmatic.commands.completion.fish
|
||||
import borgmatic.config.paths
|
||||
from borgmatic.borg import umount as borg_umount
|
||||
from borgmatic.borg import version as borg_version
|
||||
from borgmatic.commands.arguments import parse_arguments
|
||||
|
|
@ -67,6 +69,113 @@ def get_skip_actions(config, arguments):
|
|||
return skip_actions
|
||||
|
||||
|
||||
class Monitoring_hooks:
|
||||
'''
|
||||
A Python context manager for pinging monitoring hooks for the start state before the wrapped
|
||||
code and log and finish (or failure) after the wrapped code. Also responsible for
|
||||
initializing/destroying the monitoring hooks.
|
||||
|
||||
Example use as a context manager:
|
||||
|
||||
with Monitoring_hooks(config_filename, config, arguments, global_arguments):
|
||||
do_stuff()
|
||||
'''
|
||||
|
||||
def __init__(self, config_filename, config, arguments, global_arguments):
|
||||
'''
|
||||
Given a configuration filename, a configuration dict, command-line arguments as an
|
||||
argparse.Namespace, and global arguments as an argparse.Namespace, save relevant data points
|
||||
for use below.
|
||||
'''
|
||||
using_primary_action = {'create', 'prune', 'compact', 'check'}.intersection(arguments)
|
||||
self.config_filename = config_filename
|
||||
self.config = config
|
||||
self.dry_run = global_arguments.dry_run
|
||||
self.monitoring_log_level = verbosity_to_log_level(global_arguments.monitoring_verbosity)
|
||||
self.monitoring_hooks_are_activated = (
|
||||
using_primary_action and self.monitoring_log_level != DISABLED
|
||||
)
|
||||
|
||||
def __enter__(self):
|
||||
'''
|
||||
If monitoring hooks are enabled and a primary action is in use, initialize monitoring hooks
|
||||
and ping them for the "start" state.
|
||||
'''
|
||||
if not self.monitoring_hooks_are_activated:
|
||||
return
|
||||
|
||||
dispatch.call_hooks(
|
||||
'initialize_monitor',
|
||||
self.config,
|
||||
dispatch.Hook_type.MONITORING,
|
||||
self.config_filename,
|
||||
self.monitoring_log_level,
|
||||
self.dry_run,
|
||||
)
|
||||
|
||||
try:
|
||||
dispatch.call_hooks(
|
||||
'ping_monitor',
|
||||
self.config,
|
||||
dispatch.Hook_type.MONITORING,
|
||||
self.config_filename,
|
||||
monitor.State.START,
|
||||
self.monitoring_log_level,
|
||||
self.dry_run,
|
||||
)
|
||||
except (OSError, CalledProcessError) as error:
|
||||
raise ValueError(f'Error pinging monitor: {error}')
|
||||
|
||||
def __exit__(self, exception_type, exception, traceback):
|
||||
'''
|
||||
If monitoring hooks are enabled and a primary action is in use, ping monitoring hooks for
|
||||
the "log" state and also the "finish" or "fail" states (depending on whether there's an
|
||||
exception). Lastly, destroy monitoring hooks.
|
||||
'''
|
||||
if not self.monitoring_hooks_are_activated:
|
||||
return
|
||||
|
||||
# Send logs irrespective of error.
|
||||
try:
|
||||
dispatch.call_hooks(
|
||||
'ping_monitor',
|
||||
self.config,
|
||||
dispatch.Hook_type.MONITORING,
|
||||
self.config_filename,
|
||||
monitor.State.LOG,
|
||||
self.monitoring_log_level,
|
||||
self.dry_run,
|
||||
)
|
||||
except (OSError, CalledProcessError) as error:
|
||||
raise ValueError(f'Error pinging monitor: {error}')
|
||||
|
||||
try:
|
||||
dispatch.call_hooks(
|
||||
'ping_monitor',
|
||||
self.config,
|
||||
dispatch.Hook_type.MONITORING,
|
||||
self.config_filename,
|
||||
monitor.State.FAIL if exception else monitor.State.FINISH,
|
||||
self.monitoring_log_level,
|
||||
self.dry_run,
|
||||
)
|
||||
except (OSError, CalledProcessError) as error:
|
||||
# If the wrapped code errored, prefer raising that exception, as it's probably more
|
||||
# important than a monitor failing to ping.
|
||||
if exception:
|
||||
return
|
||||
|
||||
raise ValueError(f'Error pinging monitor: {error}')
|
||||
|
||||
dispatch.call_hooks(
|
||||
'destroy_monitor',
|
||||
self.config,
|
||||
dispatch.Hook_type.MONITORING,
|
||||
self.monitoring_log_level,
|
||||
self.dry_run,
|
||||
)
|
||||
|
||||
|
||||
def run_configuration(config_filename, config, config_paths, arguments):
|
||||
'''
|
||||
Given a config filename, the corresponding parsed config dict, a sequence of loaded
|
||||
|
|
@ -84,11 +193,9 @@ def run_configuration(config_filename, config, config_paths, arguments):
|
|||
remote_path = config.get('remote_path')
|
||||
retries = config.get('retries', 0)
|
||||
retry_wait = config.get('retry_wait', 0)
|
||||
repo_queue = Queue()
|
||||
encountered_error = None
|
||||
error_repository = ''
|
||||
using_primary_action = {'create', 'prune', 'compact', 'check'}.intersection(arguments)
|
||||
monitoring_log_level = verbosity_to_log_level(global_arguments.monitoring_verbosity)
|
||||
monitoring_hooks_are_activated = using_primary_action and monitoring_log_level != DISABLED
|
||||
error_repository = None
|
||||
skip_actions = get_skip_actions(config, arguments)
|
||||
|
||||
if skip_actions:
|
||||
|
|
@ -97,168 +204,105 @@ def run_configuration(config_filename, config, config_paths, arguments):
|
|||
)
|
||||
|
||||
try:
|
||||
local_borg_version = borg_version.local_borg_version(config, local_path)
|
||||
logger.debug(f'Borg {local_borg_version}')
|
||||
with Monitoring_hooks(config_filename, config, arguments, global_arguments):
|
||||
with borgmatic.hooks.command.Before_after_hooks(
|
||||
command_hooks=config.get('commands'),
|
||||
before_after='configuration',
|
||||
umask=config.get('umask'),
|
||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
||||
dry_run=global_arguments.dry_run,
|
||||
action_names=arguments.keys(),
|
||||
configuration_filename=config_filename,
|
||||
log_file=arguments['global'].log_file or '',
|
||||
):
|
||||
try:
|
||||
local_borg_version = borg_version.local_borg_version(config, local_path)
|
||||
logger.debug(f'Borg {local_borg_version}')
|
||||
except (OSError, CalledProcessError, ValueError) as error:
|
||||
yield from log_error_records(
|
||||
f'{config_filename}: Error getting local Borg version', error
|
||||
)
|
||||
return
|
||||
|
||||
for repo in config['repositories']:
|
||||
repo_queue.put(
|
||||
(repo, 0),
|
||||
)
|
||||
|
||||
while not repo_queue.empty():
|
||||
repository, retry_num = repo_queue.get()
|
||||
|
||||
with Log_prefix(repository.get('label', repository['path'])):
|
||||
logger.debug('Running actions for repository')
|
||||
timeout = retry_num * retry_wait
|
||||
if timeout:
|
||||
logger.warning(f'Sleeping {timeout}s before next retry')
|
||||
time.sleep(timeout)
|
||||
try:
|
||||
yield from run_actions(
|
||||
arguments=arguments,
|
||||
config_filename=config_filename,
|
||||
config=config,
|
||||
config_paths=config_paths,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
local_borg_version=local_borg_version,
|
||||
repository=repository,
|
||||
)
|
||||
except (OSError, CalledProcessError, ValueError) as error:
|
||||
if retry_num < retries:
|
||||
repo_queue.put(
|
||||
(repository, retry_num + 1),
|
||||
)
|
||||
tuple( # Consume the generator so as to trigger logging.
|
||||
log_error_records(
|
||||
'Error running actions for repository',
|
||||
error,
|
||||
levelno=logging.WARNING,
|
||||
log_command_error_output=True,
|
||||
)
|
||||
)
|
||||
logger.warning(f'Retrying... attempt {retry_num + 1}/{retries}')
|
||||
continue
|
||||
|
||||
if command.considered_soft_failure(error):
|
||||
continue
|
||||
|
||||
yield from log_error_records(
|
||||
'Error running actions for repository',
|
||||
error,
|
||||
)
|
||||
encountered_error = error
|
||||
error_repository = repository
|
||||
|
||||
except (OSError, CalledProcessError, ValueError) as error:
|
||||
yield from log_error_records(f'{config_filename}: Error getting local Borg version', error)
|
||||
yield from log_error_records('Error running configuration', error)
|
||||
|
||||
encountered_error = error
|
||||
|
||||
if not encountered_error:
|
||||
return
|
||||
|
||||
try:
|
||||
if monitoring_hooks_are_activated:
|
||||
dispatch.call_hooks(
|
||||
'initialize_monitor',
|
||||
config,
|
||||
dispatch.Hook_type.MONITORING,
|
||||
config_filename,
|
||||
monitoring_log_level,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
|
||||
dispatch.call_hooks(
|
||||
'ping_monitor',
|
||||
config,
|
||||
dispatch.Hook_type.MONITORING,
|
||||
config_filename,
|
||||
monitor.State.START,
|
||||
monitoring_log_level,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
command.execute_hooks(
|
||||
command.filter_hooks(
|
||||
config.get('commands'), after='error', action_names=arguments.keys()
|
||||
),
|
||||
config.get('umask'),
|
||||
borgmatic.config.paths.get_working_directory(config),
|
||||
global_arguments.dry_run,
|
||||
configuration_filename=config_filename,
|
||||
log_file=arguments['global'].log_file or '',
|
||||
repository=error_repository.get('path', '') if error_repository else '',
|
||||
repository_label=error_repository.get('label', '') if error_repository else '',
|
||||
error=encountered_error,
|
||||
output=getattr(encountered_error, 'output', ''),
|
||||
)
|
||||
except (OSError, CalledProcessError) as error:
|
||||
if command.considered_soft_failure(error):
|
||||
return
|
||||
|
||||
encountered_error = error
|
||||
yield from log_error_records(f'{config_filename}: Error pinging monitor', error)
|
||||
|
||||
if not encountered_error:
|
||||
repo_queue = Queue()
|
||||
for repo in config['repositories']:
|
||||
repo_queue.put(
|
||||
(repo, 0),
|
||||
)
|
||||
|
||||
while not repo_queue.empty():
|
||||
repository, retry_num = repo_queue.get()
|
||||
|
||||
with Log_prefix(repository.get('label', repository['path'])):
|
||||
logger.debug('Running actions for repository')
|
||||
timeout = retry_num * retry_wait
|
||||
if timeout:
|
||||
logger.warning(f'Sleeping {timeout}s before next retry')
|
||||
time.sleep(timeout)
|
||||
try:
|
||||
yield from run_actions(
|
||||
arguments=arguments,
|
||||
config_filename=config_filename,
|
||||
config=config,
|
||||
config_paths=config_paths,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
local_borg_version=local_borg_version,
|
||||
repository=repository,
|
||||
)
|
||||
except (OSError, CalledProcessError, ValueError) as error:
|
||||
if retry_num < retries:
|
||||
repo_queue.put(
|
||||
(repository, retry_num + 1),
|
||||
)
|
||||
tuple( # Consume the generator so as to trigger logging.
|
||||
log_error_records(
|
||||
'Error running actions for repository',
|
||||
error,
|
||||
levelno=logging.WARNING,
|
||||
log_command_error_output=True,
|
||||
)
|
||||
)
|
||||
logger.warning(f'Retrying... attempt {retry_num + 1}/{retries}')
|
||||
continue
|
||||
|
||||
if command.considered_soft_failure(error):
|
||||
continue
|
||||
|
||||
yield from log_error_records(
|
||||
'Error running actions for repository',
|
||||
error,
|
||||
)
|
||||
encountered_error = error
|
||||
error_repository = repository['path']
|
||||
|
||||
try:
|
||||
if monitoring_hooks_are_activated:
|
||||
# Send logs irrespective of error.
|
||||
dispatch.call_hooks(
|
||||
'ping_monitor',
|
||||
config,
|
||||
dispatch.Hook_type.MONITORING,
|
||||
config_filename,
|
||||
monitor.State.LOG,
|
||||
monitoring_log_level,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
except (OSError, CalledProcessError) as error:
|
||||
if not command.considered_soft_failure(error):
|
||||
encountered_error = error
|
||||
yield from log_error_records('Error pinging monitor', error)
|
||||
|
||||
if not encountered_error:
|
||||
try:
|
||||
if monitoring_hooks_are_activated:
|
||||
dispatch.call_hooks(
|
||||
'ping_monitor',
|
||||
config,
|
||||
dispatch.Hook_type.MONITORING,
|
||||
config_filename,
|
||||
monitor.State.FINISH,
|
||||
monitoring_log_level,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
dispatch.call_hooks(
|
||||
'destroy_monitor',
|
||||
config,
|
||||
dispatch.Hook_type.MONITORING,
|
||||
monitoring_log_level,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
except (OSError, CalledProcessError) as error:
|
||||
if command.considered_soft_failure(error):
|
||||
return
|
||||
|
||||
encountered_error = error
|
||||
yield from log_error_records(f'{config_filename}: Error pinging monitor', error)
|
||||
|
||||
if encountered_error and using_primary_action:
|
||||
try:
|
||||
command.execute_hook(
|
||||
config.get('on_error'),
|
||||
config.get('umask'),
|
||||
config_filename,
|
||||
'on-error',
|
||||
global_arguments.dry_run,
|
||||
repository=error_repository,
|
||||
error=encountered_error,
|
||||
output=getattr(encountered_error, 'output', ''),
|
||||
)
|
||||
dispatch.call_hooks(
|
||||
'ping_monitor',
|
||||
config,
|
||||
dispatch.Hook_type.MONITORING,
|
||||
config_filename,
|
||||
monitor.State.FAIL,
|
||||
monitoring_log_level,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
dispatch.call_hooks(
|
||||
'destroy_monitor',
|
||||
config,
|
||||
dispatch.Hook_type.MONITORING,
|
||||
monitoring_log_level,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
except (OSError, CalledProcessError) as error:
|
||||
if command.considered_soft_failure(error):
|
||||
return
|
||||
|
||||
yield from log_error_records(f'{config_filename}: Error running on-error hook', error)
|
||||
yield from log_error_records(f'{config_filename}: Error running after error hook', error)
|
||||
|
||||
|
||||
def run_actions(
|
||||
|
|
@ -289,6 +333,7 @@ def run_actions(
|
|||
global_arguments = arguments['global']
|
||||
dry_run_label = ' (dry run; not making any changes)' if global_arguments.dry_run else ''
|
||||
hook_context = {
|
||||
'configuration_filename': config_filename,
|
||||
'repository_label': repository.get('label', ''),
|
||||
'log_file': global_arguments.log_file if global_arguments.log_file else '',
|
||||
# Deprecated: For backwards compatibility with borgmatic < 1.6.0.
|
||||
|
|
@ -297,240 +342,248 @@ def run_actions(
|
|||
}
|
||||
skip_actions = set(get_skip_actions(config, arguments))
|
||||
|
||||
command.execute_hook(
|
||||
config.get('before_actions'),
|
||||
config.get('umask'),
|
||||
config_filename,
|
||||
'pre-actions',
|
||||
global_arguments.dry_run,
|
||||
with borgmatic.hooks.command.Before_after_hooks(
|
||||
command_hooks=config.get('commands'),
|
||||
before_after='repository',
|
||||
umask=config.get('umask'),
|
||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
||||
dry_run=global_arguments.dry_run,
|
||||
action_names=arguments.keys(),
|
||||
**hook_context,
|
||||
)
|
||||
):
|
||||
for action_name, action_arguments in arguments.items():
|
||||
if action_name == 'global':
|
||||
continue
|
||||
|
||||
for action_name, action_arguments in arguments.items():
|
||||
if action_name == 'repo-create' and action_name not in skip_actions:
|
||||
borgmatic.actions.repo_create.run_repo_create(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'transfer' and action_name not in skip_actions:
|
||||
borgmatic.actions.transfer.run_transfer(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'create' and action_name not in skip_actions:
|
||||
yield from borgmatic.actions.create.run_create(
|
||||
config_filename,
|
||||
repository,
|
||||
config,
|
||||
config_paths,
|
||||
hook_context,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
dry_run_label,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'prune' and action_name not in skip_actions:
|
||||
borgmatic.actions.prune.run_prune(
|
||||
config_filename,
|
||||
repository,
|
||||
config,
|
||||
hook_context,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
dry_run_label,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'compact' and action_name not in skip_actions:
|
||||
borgmatic.actions.compact.run_compact(
|
||||
config_filename,
|
||||
repository,
|
||||
config,
|
||||
hook_context,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
dry_run_label,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'check' and action_name not in skip_actions:
|
||||
if checks.repository_enabled_for_checks(repository, config):
|
||||
borgmatic.actions.check.run_check(
|
||||
config_filename,
|
||||
repository,
|
||||
config,
|
||||
hook_context,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'extract' and action_name not in skip_actions:
|
||||
borgmatic.actions.extract.run_extract(
|
||||
config_filename,
|
||||
repository,
|
||||
config,
|
||||
hook_context,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'export-tar' and action_name not in skip_actions:
|
||||
borgmatic.actions.export_tar.run_export_tar(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'mount' and action_name not in skip_actions:
|
||||
borgmatic.actions.mount.run_mount(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'restore' and action_name not in skip_actions:
|
||||
borgmatic.actions.restore.run_restore(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'repo-list' and action_name not in skip_actions:
|
||||
yield from borgmatic.actions.repo_list.run_repo_list(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'list' and action_name not in skip_actions:
|
||||
yield from borgmatic.actions.list.run_list(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'repo-info' and action_name not in skip_actions:
|
||||
yield from borgmatic.actions.repo_info.run_repo_info(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'info' and action_name not in skip_actions:
|
||||
yield from borgmatic.actions.info.run_info(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'break-lock' and action_name not in skip_actions:
|
||||
borgmatic.actions.break_lock.run_break_lock(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'export' and action_name not in skip_actions:
|
||||
borgmatic.actions.export_key.run_export_key(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'change-passphrase' and action_name not in skip_actions:
|
||||
borgmatic.actions.change_passphrase.run_change_passphrase(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'delete' and action_name not in skip_actions:
|
||||
borgmatic.actions.delete.run_delete(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'repo-delete' and action_name not in skip_actions:
|
||||
borgmatic.actions.repo_delete.run_repo_delete(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'borg' and action_name not in skip_actions:
|
||||
borgmatic.actions.borg.run_borg(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
|
||||
command.execute_hook(
|
||||
config.get('after_actions'),
|
||||
config.get('umask'),
|
||||
config_filename,
|
||||
'post-actions',
|
||||
global_arguments.dry_run,
|
||||
**hook_context,
|
||||
)
|
||||
with borgmatic.hooks.command.Before_after_hooks(
|
||||
command_hooks=config.get('commands'),
|
||||
before_after='action',
|
||||
umask=config.get('umask'),
|
||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
||||
dry_run=global_arguments.dry_run,
|
||||
action_names=arguments.keys(),
|
||||
**hook_context,
|
||||
):
|
||||
if action_name == 'repo-create' and action_name not in skip_actions:
|
||||
borgmatic.actions.repo_create.run_repo_create(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'transfer' and action_name not in skip_actions:
|
||||
borgmatic.actions.transfer.run_transfer(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'create' and action_name not in skip_actions:
|
||||
yield from borgmatic.actions.create.run_create(
|
||||
config_filename,
|
||||
repository,
|
||||
config,
|
||||
config_paths,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
dry_run_label,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'prune' and action_name not in skip_actions:
|
||||
borgmatic.actions.prune.run_prune(
|
||||
config_filename,
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
dry_run_label,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'compact' and action_name not in skip_actions:
|
||||
borgmatic.actions.compact.run_compact(
|
||||
config_filename,
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
dry_run_label,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'check' and action_name not in skip_actions:
|
||||
if checks.repository_enabled_for_checks(repository, config):
|
||||
borgmatic.actions.check.run_check(
|
||||
config_filename,
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'extract' and action_name not in skip_actions:
|
||||
borgmatic.actions.extract.run_extract(
|
||||
config_filename,
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'export-tar' and action_name not in skip_actions:
|
||||
borgmatic.actions.export_tar.run_export_tar(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'mount' and action_name not in skip_actions:
|
||||
borgmatic.actions.mount.run_mount(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'restore' and action_name not in skip_actions:
|
||||
borgmatic.actions.restore.run_restore(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'repo-list' and action_name not in skip_actions:
|
||||
yield from borgmatic.actions.repo_list.run_repo_list(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'list' and action_name not in skip_actions:
|
||||
yield from borgmatic.actions.list.run_list(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'repo-info' and action_name not in skip_actions:
|
||||
yield from borgmatic.actions.repo_info.run_repo_info(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'info' and action_name not in skip_actions:
|
||||
yield from borgmatic.actions.info.run_info(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'break-lock' and action_name not in skip_actions:
|
||||
borgmatic.actions.break_lock.run_break_lock(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'export' and action_name not in skip_actions:
|
||||
borgmatic.actions.export_key.run_export_key(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'import' and action_name not in skip_actions:
|
||||
borgmatic.actions.import_key.run_import_key(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'change-passphrase' and action_name not in skip_actions:
|
||||
borgmatic.actions.change_passphrase.run_change_passphrase(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'delete' and action_name not in skip_actions:
|
||||
borgmatic.actions.delete.run_delete(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'repo-delete' and action_name not in skip_actions:
|
||||
borgmatic.actions.repo_delete.run_repo_delete(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'borg' and action_name not in skip_actions:
|
||||
borgmatic.actions.borg.run_borg(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
|
||||
|
||||
def load_configurations(config_filenames, overrides=None, resolve_env=True):
|
||||
|
|
@ -810,19 +863,21 @@ def collect_configuration_run_summary_logs(configs, config_paths, arguments):
|
|||
)
|
||||
return
|
||||
|
||||
if 'create' in arguments:
|
||||
try:
|
||||
for config_filename, config in configs.items():
|
||||
command.execute_hook(
|
||||
config.get('before_everything'),
|
||||
config.get('umask'),
|
||||
config_filename,
|
||||
'pre-everything',
|
||||
arguments['global'].dry_run,
|
||||
)
|
||||
except (CalledProcessError, ValueError, OSError) as error:
|
||||
yield from log_error_records('Error running pre-everything hook', error)
|
||||
return
|
||||
try:
|
||||
for config_filename, config in configs.items():
|
||||
command.execute_hooks(
|
||||
command.filter_hooks(
|
||||
config.get('commands'), before='everything', action_names=arguments.keys()
|
||||
),
|
||||
config.get('umask'),
|
||||
borgmatic.config.paths.get_working_directory(config),
|
||||
arguments['global'].dry_run,
|
||||
configuration_filename=config_filename,
|
||||
log_file=arguments['global'].log_file or '',
|
||||
)
|
||||
except (CalledProcessError, ValueError, OSError) as error:
|
||||
yield from log_error_records('Error running before everything hook', error)
|
||||
return
|
||||
|
||||
# Execute the actions corresponding to each configuration file.
|
||||
json_results = []
|
||||
|
|
@ -830,6 +885,7 @@ def collect_configuration_run_summary_logs(configs, config_paths, arguments):
|
|||
for config_filename, config in configs.items():
|
||||
with Log_prefix(config_filename):
|
||||
results = list(run_configuration(config_filename, config, config_paths, arguments))
|
||||
|
||||
error_logs = tuple(
|
||||
result for result in results if isinstance(result, logging.LogRecord)
|
||||
)
|
||||
|
|
@ -862,18 +918,20 @@ def collect_configuration_run_summary_logs(configs, config_paths, arguments):
|
|||
if json_results:
|
||||
sys.stdout.write(json.dumps(json_results))
|
||||
|
||||
if 'create' in arguments:
|
||||
try:
|
||||
for config_filename, config in configs.items():
|
||||
command.execute_hook(
|
||||
config.get('after_everything'),
|
||||
config.get('umask'),
|
||||
config_filename,
|
||||
'post-everything',
|
||||
arguments['global'].dry_run,
|
||||
)
|
||||
except (CalledProcessError, ValueError, OSError) as error:
|
||||
yield from log_error_records('Error running post-everything hook', error)
|
||||
try:
|
||||
for config_filename, config in configs.items():
|
||||
command.execute_hooks(
|
||||
command.filter_hooks(
|
||||
config.get('commands'), after='everything', action_names=arguments.keys()
|
||||
),
|
||||
config.get('umask'),
|
||||
borgmatic.config.paths.get_working_directory(config),
|
||||
arguments['global'].dry_run,
|
||||
configuration_filename=config_filename,
|
||||
log_file=arguments['global'].log_file or '',
|
||||
)
|
||||
except (CalledProcessError, ValueError, OSError) as error:
|
||||
yield from log_error_records('Error running after everything hook', error)
|
||||
|
||||
|
||||
def exit_with_help_link(): # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import collections
|
||||
import io
|
||||
import itertools
|
||||
import os
|
||||
import re
|
||||
|
||||
|
|
@ -24,20 +25,31 @@ def insert_newline_before_comment(config, field_name):
|
|||
def get_properties(schema):
|
||||
'''
|
||||
Given a schema dict, return its properties. But if it's got sub-schemas with multiple different
|
||||
potential properties, returned their merged properties instead.
|
||||
potential properties, returned their merged properties instead (interleaved so the first
|
||||
properties of each sub-schema come first). The idea is that the user should see all possible
|
||||
options even if they're not all possible together.
|
||||
'''
|
||||
if 'oneOf' in schema:
|
||||
return dict(
|
||||
collections.ChainMap(*[sub_schema['properties'] for sub_schema in schema['oneOf']])
|
||||
item
|
||||
for item in itertools.chain(
|
||||
*itertools.zip_longest(
|
||||
*[sub_schema['properties'].items() for sub_schema in schema['oneOf']]
|
||||
)
|
||||
)
|
||||
if item is not None
|
||||
)
|
||||
|
||||
return schema['properties']
|
||||
|
||||
|
||||
def schema_to_sample_configuration(schema, level=0, parent_is_sequence=False):
|
||||
def schema_to_sample_configuration(schema, source_config=None, level=0, parent_is_sequence=False):
|
||||
'''
|
||||
Given a loaded configuration schema, generate and return sample config for it. Include comments
|
||||
for each option based on the schema "description".
|
||||
Given a loaded configuration schema and a source configuration, generate and return sample
|
||||
config for the schema. Include comments for each option based on the schema "description".
|
||||
|
||||
If a source config is given, walk it alongside the given schema so that both can be taken into
|
||||
account when commenting out particular options in add_comments_to_configuration_object().
|
||||
'''
|
||||
schema_type = schema.get('type')
|
||||
example = schema.get('example')
|
||||
|
|
@ -47,19 +59,31 @@ def schema_to_sample_configuration(schema, level=0, parent_is_sequence=False):
|
|||
|
||||
if schema_type == 'array' or (isinstance(schema_type, list) and 'array' in schema_type):
|
||||
config = ruamel.yaml.comments.CommentedSeq(
|
||||
[schema_to_sample_configuration(schema['items'], level, parent_is_sequence=True)]
|
||||
[
|
||||
schema_to_sample_configuration(
|
||||
schema['items'], source_config, level, parent_is_sequence=True
|
||||
)
|
||||
]
|
||||
)
|
||||
add_comments_to_configuration_sequence(config, schema, indent=(level * INDENT))
|
||||
elif schema_type == 'object' or (isinstance(schema_type, list) and 'object' in schema_type):
|
||||
if source_config and isinstance(source_config, list) and isinstance(source_config[0], dict):
|
||||
source_config = dict(collections.ChainMap(*source_config))
|
||||
|
||||
config = ruamel.yaml.comments.CommentedMap(
|
||||
[
|
||||
(field_name, schema_to_sample_configuration(sub_schema, level + 1))
|
||||
(
|
||||
field_name,
|
||||
schema_to_sample_configuration(
|
||||
sub_schema, (source_config or {}).get(field_name, {}), level + 1
|
||||
),
|
||||
)
|
||||
for field_name, sub_schema in get_properties(schema).items()
|
||||
]
|
||||
)
|
||||
indent = (level * INDENT) + (SEQUENCE_INDENT if parent_is_sequence else 0)
|
||||
add_comments_to_configuration_object(
|
||||
config, schema, indent=indent, skip_first=parent_is_sequence
|
||||
config, schema, source_config, indent=indent, skip_first=parent_is_sequence
|
||||
)
|
||||
else:
|
||||
raise ValueError(f'Schema at level {level} is unsupported: {schema}')
|
||||
|
|
@ -179,14 +203,21 @@ def add_comments_to_configuration_sequence(config, schema, indent=0):
|
|||
return
|
||||
|
||||
|
||||
REQUIRED_KEYS = {'source_directories', 'repositories', 'keep_daily'}
|
||||
DEFAULT_KEYS = {'source_directories', 'repositories', 'keep_daily'}
|
||||
COMMENTED_OUT_SENTINEL = 'COMMENT_OUT'
|
||||
|
||||
|
||||
def add_comments_to_configuration_object(config, schema, indent=0, skip_first=False):
|
||||
def add_comments_to_configuration_object(
|
||||
config, schema, source_config=None, indent=0, skip_first=False
|
||||
):
|
||||
'''
|
||||
Using descriptions from a schema as a source, add those descriptions as comments to the given
|
||||
config mapping, before each field. Indent the comment the given number of characters.
|
||||
configuration dict, putting them before each field. Indent the comment the given number of
|
||||
characters.
|
||||
|
||||
And a sentinel for commenting out options that are neither in DEFAULT_KEYS nor the the given
|
||||
source configuration dict. The idea is that any options used in the source configuration should
|
||||
stay active in the generated configuration.
|
||||
'''
|
||||
for index, field_name in enumerate(config.keys()):
|
||||
if skip_first and index == 0:
|
||||
|
|
@ -195,10 +226,12 @@ def add_comments_to_configuration_object(config, schema, indent=0, skip_first=Fa
|
|||
field_schema = get_properties(schema).get(field_name, {})
|
||||
description = field_schema.get('description', '').strip()
|
||||
|
||||
# If this is an optional key, add an indicator to the comment flagging it to be commented
|
||||
# If this isn't a default key, add an indicator to the comment flagging it to be commented
|
||||
# out from the sample configuration. This sentinel is consumed by downstream processing that
|
||||
# does the actual commenting out.
|
||||
if field_name not in REQUIRED_KEYS:
|
||||
if field_name not in DEFAULT_KEYS and (
|
||||
source_config is None or field_name not in source_config
|
||||
):
|
||||
description = (
|
||||
'\n'.join((description, COMMENTED_OUT_SENTINEL))
|
||||
if description
|
||||
|
|
@ -218,21 +251,6 @@ def add_comments_to_configuration_object(config, schema, indent=0, skip_first=Fa
|
|||
RUAMEL_YAML_COMMENTS_INDEX = 1
|
||||
|
||||
|
||||
def remove_commented_out_sentinel(config, field_name):
|
||||
'''
|
||||
Given a configuration CommentedMap and a top-level field name in it, remove any "commented out"
|
||||
sentinel found at the end of its YAML comments. This prevents the given field name from getting
|
||||
commented out by downstream processing that consumes the sentinel.
|
||||
'''
|
||||
try:
|
||||
last_comment_value = config.ca.items[field_name][RUAMEL_YAML_COMMENTS_INDEX][-1].value
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
if last_comment_value == f'# {COMMENTED_OUT_SENTINEL}\n':
|
||||
config.ca.items[field_name][RUAMEL_YAML_COMMENTS_INDEX].pop()
|
||||
|
||||
|
||||
def merge_source_configuration_into_destination(destination_config, source_config):
|
||||
'''
|
||||
Deep merge the given source configuration dict into the destination configuration CommentedMap,
|
||||
|
|
@ -247,12 +265,6 @@ def merge_source_configuration_into_destination(destination_config, source_confi
|
|||
return source_config
|
||||
|
||||
for field_name, source_value in source_config.items():
|
||||
# Since this key/value is from the source configuration, leave it uncommented and remove any
|
||||
# sentinel that would cause it to get commented out.
|
||||
remove_commented_out_sentinel(
|
||||
ruamel.yaml.comments.CommentedMap(destination_config), field_name
|
||||
)
|
||||
|
||||
# This is a mapping. Recurse for this key/value.
|
||||
if isinstance(source_value, collections.abc.Mapping):
|
||||
destination_config[field_name] = merge_source_configuration_into_destination(
|
||||
|
|
@ -298,7 +310,7 @@ def generate_sample_configuration(
|
|||
normalize.normalize(source_filename, source_config)
|
||||
|
||||
destination_config = merge_source_configuration_into_destination(
|
||||
schema_to_sample_configuration(schema), source_config
|
||||
schema_to_sample_configuration(schema, source_config), source_config
|
||||
)
|
||||
|
||||
if dry_run:
|
||||
|
|
|
|||
|
|
@ -58,6 +58,90 @@ def normalize_sections(config_filename, config):
|
|||
return []
|
||||
|
||||
|
||||
def make_command_hook_deprecation_log(config_filename, option_name): # pragma: no cover
|
||||
'''
|
||||
Given a configuration filename and the name of a configuration option, return a deprecation
|
||||
warning log for it.
|
||||
'''
|
||||
return logging.makeLogRecord(
|
||||
dict(
|
||||
levelno=logging.WARNING,
|
||||
levelname='WARNING',
|
||||
msg=f'{config_filename}: {option_name} is deprecated and support will be removed from a future release. Use commands: instead.',
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def normalize_commands(config_filename, config):
|
||||
'''
|
||||
Given a configuration filename and a configuration dict, transform any "before_*"- and
|
||||
"after_*"-style command hooks into "commands:".
|
||||
'''
|
||||
logs = []
|
||||
|
||||
# Normalize "before_actions" and "after_actions".
|
||||
for preposition in ('before', 'after'):
|
||||
option_name = f'{preposition}_actions'
|
||||
commands = config.pop(option_name, None)
|
||||
|
||||
if commands:
|
||||
logs.append(make_command_hook_deprecation_log(config_filename, option_name))
|
||||
config.setdefault('commands', []).append(
|
||||
{
|
||||
preposition: 'repository',
|
||||
'run': commands,
|
||||
}
|
||||
)
|
||||
|
||||
# Normalize "before_backup", "before_prune", "after_backup", "after_prune", etc.
|
||||
for action_name in ('create', 'prune', 'compact', 'check', 'extract'):
|
||||
for preposition in ('before', 'after'):
|
||||
option_name = f'{preposition}_{"backup" if action_name == "create" else action_name}'
|
||||
commands = config.pop(option_name, None)
|
||||
|
||||
if not commands:
|
||||
continue
|
||||
|
||||
logs.append(make_command_hook_deprecation_log(config_filename, option_name))
|
||||
config.setdefault('commands', []).append(
|
||||
{
|
||||
preposition: 'action',
|
||||
'when': [action_name],
|
||||
'run': commands,
|
||||
}
|
||||
)
|
||||
|
||||
# Normalize "on_error".
|
||||
commands = config.pop('on_error', None)
|
||||
|
||||
if commands:
|
||||
logs.append(make_command_hook_deprecation_log(config_filename, 'on_error'))
|
||||
config.setdefault('commands', []).append(
|
||||
{
|
||||
'after': 'error',
|
||||
'when': ['create', 'prune', 'compact', 'check'],
|
||||
'run': commands,
|
||||
}
|
||||
)
|
||||
|
||||
# Normalize "before_everything" and "after_everything".
|
||||
for preposition in ('before', 'after'):
|
||||
option_name = f'{preposition}_everything'
|
||||
commands = config.pop(option_name, None)
|
||||
|
||||
if commands:
|
||||
logs.append(make_command_hook_deprecation_log(config_filename, option_name))
|
||||
config.setdefault('commands', []).append(
|
||||
{
|
||||
preposition: 'everything',
|
||||
'when': ['create'],
|
||||
'run': commands,
|
||||
}
|
||||
)
|
||||
|
||||
return logs
|
||||
|
||||
|
||||
def normalize(config_filename, config):
|
||||
'''
|
||||
Given a configuration filename and a configuration dict of its loaded contents, apply particular
|
||||
|
|
@ -67,6 +151,7 @@ def normalize(config_filename, config):
|
|||
Raise ValueError the configuration cannot be normalized.
|
||||
'''
|
||||
logs = normalize_sections(config_filename, config)
|
||||
logs += normalize_commands(config_filename, config)
|
||||
|
||||
if config.get('borgmatic_source_directory'):
|
||||
logs.append(
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ class Runtime_directory:
|
|||
'''
|
||||
return self.runtime_path
|
||||
|
||||
def __exit__(self, exception, value, traceback):
|
||||
def __exit__(self, exception_type, exception, traceback):
|
||||
'''
|
||||
Delete any temporary directory that was created as part of initialization.
|
||||
'''
|
||||
|
|
|
|||
|
|
@ -796,8 +796,9 @@ properties:
|
|||
items:
|
||||
type: string
|
||||
description: |
|
||||
List of one or more shell commands or scripts to execute before all
|
||||
the actions for each repository.
|
||||
Deprecated. Use "commands:" instead. List of one or more shell
|
||||
commands or scripts to execute before all the actions for each
|
||||
repository.
|
||||
example:
|
||||
- "echo Starting actions."
|
||||
before_backup:
|
||||
|
|
@ -805,8 +806,9 @@ properties:
|
|||
items:
|
||||
type: string
|
||||
description: |
|
||||
List of one or more shell commands or scripts to execute before
|
||||
creating a backup, run once per repository.
|
||||
Deprecated. Use "commands:" instead. List of one or more shell
|
||||
commands or scripts to execute before creating a backup, run once
|
||||
per repository.
|
||||
example:
|
||||
- "echo Starting a backup."
|
||||
before_prune:
|
||||
|
|
@ -814,8 +816,9 @@ properties:
|
|||
items:
|
||||
type: string
|
||||
description: |
|
||||
List of one or more shell commands or scripts to execute before
|
||||
pruning, run once per repository.
|
||||
Deprecated. Use "commands:" instead. List of one or more shell
|
||||
commands or scripts to execute before pruning, run once per
|
||||
repository.
|
||||
example:
|
||||
- "echo Starting pruning."
|
||||
before_compact:
|
||||
|
|
@ -823,8 +826,9 @@ properties:
|
|||
items:
|
||||
type: string
|
||||
description: |
|
||||
List of one or more shell commands or scripts to execute before
|
||||
compaction, run once per repository.
|
||||
Deprecated. Use "commands:" instead. List of one or more shell
|
||||
commands or scripts to execute before compaction, run once per
|
||||
repository.
|
||||
example:
|
||||
- "echo Starting compaction."
|
||||
before_check:
|
||||
|
|
@ -832,8 +836,9 @@ properties:
|
|||
items:
|
||||
type: string
|
||||
description: |
|
||||
List of one or more shell commands or scripts to execute before
|
||||
consistency checks, run once per repository.
|
||||
Deprecated. Use "commands:" instead. List of one or more shell
|
||||
commands or scripts to execute before consistency checks, run once
|
||||
per repository.
|
||||
example:
|
||||
- "echo Starting checks."
|
||||
before_extract:
|
||||
|
|
@ -841,8 +846,9 @@ properties:
|
|||
items:
|
||||
type: string
|
||||
description: |
|
||||
List of one or more shell commands or scripts to execute before
|
||||
extracting a backup, run once per repository.
|
||||
Deprecated. Use "commands:" instead. List of one or more shell
|
||||
commands or scripts to execute before extracting a backup, run once
|
||||
per repository.
|
||||
example:
|
||||
- "echo Starting extracting."
|
||||
after_backup:
|
||||
|
|
@ -850,8 +856,9 @@ properties:
|
|||
items:
|
||||
type: string
|
||||
description: |
|
||||
List of one or more shell commands or scripts to execute after
|
||||
creating a backup, run once per repository.
|
||||
Deprecated. Use "commands:" instead. List of one or more shell
|
||||
commands or scripts to execute after creating a backup, run once per
|
||||
repository.
|
||||
example:
|
||||
- "echo Finished a backup."
|
||||
after_compact:
|
||||
|
|
@ -859,8 +866,9 @@ properties:
|
|||
items:
|
||||
type: string
|
||||
description: |
|
||||
List of one or more shell commands or scripts to execute after
|
||||
compaction, run once per repository.
|
||||
Deprecated. Use "commands:" instead. List of one or more shell
|
||||
commands or scripts to execute after compaction, run once per
|
||||
repository.
|
||||
example:
|
||||
- "echo Finished compaction."
|
||||
after_prune:
|
||||
|
|
@ -868,8 +876,9 @@ properties:
|
|||
items:
|
||||
type: string
|
||||
description: |
|
||||
List of one or more shell commands or scripts to execute after
|
||||
pruning, run once per repository.
|
||||
Deprecated. Use "commands:" instead. List of one or more shell
|
||||
commands or scripts to execute after pruning, run once per
|
||||
repository.
|
||||
example:
|
||||
- "echo Finished pruning."
|
||||
after_check:
|
||||
|
|
@ -877,8 +886,9 @@ properties:
|
|||
items:
|
||||
type: string
|
||||
description: |
|
||||
List of one or more shell commands or scripts to execute after
|
||||
consistency checks, run once per repository.
|
||||
Deprecated. Use "commands:" instead. List of one or more shell
|
||||
commands or scripts to execute after consistency checks, run once
|
||||
per repository.
|
||||
example:
|
||||
- "echo Finished checks."
|
||||
after_extract:
|
||||
|
|
@ -886,8 +896,9 @@ properties:
|
|||
items:
|
||||
type: string
|
||||
description: |
|
||||
List of one or more shell commands or scripts to execute after
|
||||
extracting a backup, run once per repository.
|
||||
Deprecated. Use "commands:" instead. List of one or more shell
|
||||
commands or scripts to execute after extracting a backup, run once
|
||||
per repository.
|
||||
example:
|
||||
- "echo Finished extracting."
|
||||
after_actions:
|
||||
|
|
@ -895,8 +906,9 @@ properties:
|
|||
items:
|
||||
type: string
|
||||
description: |
|
||||
List of one or more shell commands or scripts to execute after all
|
||||
actions for each repository.
|
||||
Deprecated. Use "commands:" instead. List of one or more shell
|
||||
commands or scripts to execute after all actions for each
|
||||
repository.
|
||||
example:
|
||||
- "echo Finished actions."
|
||||
on_error:
|
||||
|
|
@ -904,9 +916,10 @@ properties:
|
|||
items:
|
||||
type: string
|
||||
description: |
|
||||
List of one or more shell commands or scripts to execute when an
|
||||
exception occurs during a "create", "prune", "compact", or "check"
|
||||
action or an associated before/after hook.
|
||||
Deprecated. Use "commands:" instead. List of one or more shell
|
||||
commands or scripts to execute when an exception occurs during a
|
||||
"create", "prune", "compact", or "check" action or an associated
|
||||
before/after hook.
|
||||
example:
|
||||
- "echo Error during create/prune/compact/check."
|
||||
before_everything:
|
||||
|
|
@ -914,10 +927,10 @@ properties:
|
|||
items:
|
||||
type: string
|
||||
description: |
|
||||
List of one or more shell commands or scripts to execute before
|
||||
running all actions (if one of them is "create"). These are
|
||||
collected from all configuration files and then run once before all
|
||||
of them (prior to all actions).
|
||||
Deprecated. Use "commands:" instead. List of one or more shell
|
||||
commands or scripts to execute before running all actions (if one of
|
||||
them is "create"). These are collected from all configuration files
|
||||
and then run once before all of them (prior to all actions).
|
||||
example:
|
||||
- "echo Starting actions."
|
||||
after_everything:
|
||||
|
|
@ -925,12 +938,148 @@ properties:
|
|||
items:
|
||||
type: string
|
||||
description: |
|
||||
List of one or more shell commands or scripts to execute after
|
||||
running all actions (if one of them is "create"). These are
|
||||
collected from all configuration files and then run once after all
|
||||
of them (after any action).
|
||||
Deprecated. Use "commands:" instead. List of one or more shell
|
||||
commands or scripts to execute after running all actions (if one of
|
||||
them is "create"). These are collected from all configuration files
|
||||
and then run once after all of them (after any action).
|
||||
example:
|
||||
- "echo Completed actions."
|
||||
commands:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
oneOf:
|
||||
- required: [before, run]
|
||||
additionalProperties: false
|
||||
properties:
|
||||
before:
|
||||
type: string
|
||||
enum:
|
||||
- action
|
||||
- repository
|
||||
- configuration
|
||||
- everything
|
||||
description: |
|
||||
Name for the point in borgmatic's execution that
|
||||
the commands should be run before (required if
|
||||
"after" isn't set):
|
||||
* "action" runs before each action for each
|
||||
repository.
|
||||
* "repository" runs before all actions for each
|
||||
repository.
|
||||
* "configuration" runs before all actions and
|
||||
repositories in the current configuration file.
|
||||
* "everything" runs before all configuration
|
||||
files.
|
||||
example: action
|
||||
when:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- repo-create
|
||||
- transfer
|
||||
- prune
|
||||
- compact
|
||||
- create
|
||||
- check
|
||||
- delete
|
||||
- extract
|
||||
- config
|
||||
- export-tar
|
||||
- mount
|
||||
- umount
|
||||
- repo-delete
|
||||
- restore
|
||||
- repo-list
|
||||
- list
|
||||
- repo-info
|
||||
- info
|
||||
- break-lock
|
||||
- key
|
||||
- borg
|
||||
description: |
|
||||
List of actions for which the commands will be
|
||||
run. Defaults to running for all actions.
|
||||
example: [create, prune, compact, check]
|
||||
run:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: |
|
||||
List of one or more shell commands or scripts to
|
||||
run when this command hook is triggered. Required.
|
||||
example:
|
||||
- "echo Doing stuff."
|
||||
- required: [after, run]
|
||||
additionalProperties: false
|
||||
properties:
|
||||
after:
|
||||
type: string
|
||||
enum:
|
||||
- action
|
||||
- repository
|
||||
- configuration
|
||||
- everything
|
||||
- error
|
||||
description: |
|
||||
Name for the point in borgmatic's execution that
|
||||
the commands should be run after (required if
|
||||
"before" isn't set):
|
||||
* "action" runs after each action for each
|
||||
repository.
|
||||
* "repository" runs after all actions for each
|
||||
repository.
|
||||
* "configuration" runs after all actions and
|
||||
repositories in the current configuration file.
|
||||
* "everything" runs after all configuration
|
||||
files.
|
||||
* "error" runs after an error occurs.
|
||||
example: action
|
||||
when:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- repo-create
|
||||
- transfer
|
||||
- prune
|
||||
- compact
|
||||
- create
|
||||
- check
|
||||
- delete
|
||||
- extract
|
||||
- config
|
||||
- export-tar
|
||||
- mount
|
||||
- umount
|
||||
- repo-delete
|
||||
- restore
|
||||
- repo-list
|
||||
- list
|
||||
- repo-info
|
||||
- info
|
||||
- break-lock
|
||||
- key
|
||||
- borg
|
||||
description: |
|
||||
Only trigger the hook when borgmatic is run with
|
||||
particular actions listed here. Defaults to
|
||||
running for all actions.
|
||||
example: [create, prune, compact, check]
|
||||
run:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: |
|
||||
List of one or more shell commands or scripts to
|
||||
run when this command hook is triggered. Required.
|
||||
example:
|
||||
- "echo Doing stuff."
|
||||
description: |
|
||||
List of one or more command hooks to execute, triggered at
|
||||
particular points during borgmatic's execution. For each command
|
||||
hook, specify one of "before" or "after", not both.
|
||||
bootstrap:
|
||||
type: object
|
||||
properties:
|
||||
|
|
@ -1088,11 +1237,11 @@ properties:
|
|||
Command to use instead of "pg_dump" or "pg_dumpall".
|
||||
This can be used to run a specific pg_dump version
|
||||
(e.g., one inside a running container). If you run it
|
||||
from within a container, make sure to mount your
|
||||
host's ".borgmatic" folder into the container using
|
||||
the same directory structure. Defaults to "pg_dump"
|
||||
for single database dump or "pg_dumpall" to dump all
|
||||
databases.
|
||||
from within a container, make sure to mount the path in
|
||||
the "user_runtime_directory" option from the host into
|
||||
the container at the same location. Defaults to
|
||||
"pg_dump" for single database dump or "pg_dumpall" to
|
||||
dump all databases.
|
||||
example: docker exec my_pg_container pg_dump
|
||||
pg_restore_command:
|
||||
type: string
|
||||
|
|
@ -1229,10 +1378,11 @@ properties:
|
|||
description: |
|
||||
Command to use instead of "mariadb-dump". This can be
|
||||
used to run a specific mariadb_dump version (e.g., one
|
||||
inside a running container). If you run it from within
|
||||
a container, make sure to mount your host's
|
||||
".borgmatic" folder into the container using the same
|
||||
directory structure. Defaults to "mariadb-dump".
|
||||
inside a running container). If you run it from within a
|
||||
container, make sure to mount the path in the
|
||||
"user_runtime_directory" option from the host into the
|
||||
container at the same location. Defaults to
|
||||
"mariadb-dump".
|
||||
example: docker exec mariadb_container mariadb-dump
|
||||
mariadb_command:
|
||||
type: string
|
||||
|
|
@ -1371,12 +1521,12 @@ properties:
|
|||
mysql_dump_command:
|
||||
type: string
|
||||
description: |
|
||||
Command to use instead of "mysqldump". This can be
|
||||
used to run a specific mysql_dump version (e.g., one
|
||||
inside a running container). If you run it from within
|
||||
a container, make sure to mount your host's
|
||||
".borgmatic" folder into the container using the same
|
||||
directory structure. Defaults to "mysqldump".
|
||||
Command to use instead of "mysqldump". This can be used
|
||||
to run a specific mysql_dump version (e.g., one inside a
|
||||
running container). If you run it from within a
|
||||
container, make sure to mount the path in the
|
||||
"user_runtime_directory" option from the host into the
|
||||
container at the same location. Defaults to "mysqldump".
|
||||
example: docker exec mysql_container mysqldump
|
||||
mysql_command:
|
||||
type: string
|
||||
|
|
@ -1463,6 +1613,24 @@ properties:
|
|||
Path to the SQLite database file to restore to. Defaults
|
||||
to the "path" option.
|
||||
example: /var/lib/sqlite/users.db
|
||||
sqlite_command:
|
||||
type: string
|
||||
description: |
|
||||
Command to use instead of "sqlite3". This can be used to
|
||||
run a specific sqlite3 version (e.g., one inside a
|
||||
running container). If you run it from within a
|
||||
container, make sure to mount the path in the
|
||||
"user_runtime_directory" option from the host into the
|
||||
container at the same location. Defaults to "sqlite3".
|
||||
example: docker exec sqlite_container sqlite3
|
||||
sqlite_restore_command:
|
||||
type: string
|
||||
description: |
|
||||
Command to run when restoring a database instead
|
||||
of "sqlite3". This can be used to run a specific
|
||||
sqlite3 version (e.g., one inside a running container).
|
||||
Defaults to "sqlite3".
|
||||
example: docker exec sqlite_container sqlite3
|
||||
mongodb_databases:
|
||||
type: array
|
||||
items:
|
||||
|
|
@ -1558,6 +1726,25 @@ properties:
|
|||
dump command, without performing any validation on them.
|
||||
See mongorestore documentation for details.
|
||||
example: --restoreDbUsersAndRoles
|
||||
mongodump_command:
|
||||
type: string
|
||||
description: |
|
||||
Command to use instead of "mongodump". This can be used
|
||||
to run a specific mongodump version (e.g., one inside a
|
||||
running container). If you run it from within a
|
||||
container, make sure to mount the path in the
|
||||
"user_runtime_directory" option from the host into the
|
||||
container at the same location. Defaults to
|
||||
"mongodump".
|
||||
example: docker exec mongodb_container mongodump
|
||||
mongorestore_command:
|
||||
type: string
|
||||
description: |
|
||||
Command to run when restoring a database instead of
|
||||
"mongorestore". This can be used to run a specific
|
||||
mongorestore version (e.g., one inside a running
|
||||
container). Defaults to "mongorestore".
|
||||
example: docker exec mongodb_container mongorestore
|
||||
description: |
|
||||
List of one or more MongoDB databases to dump before creating a
|
||||
backup, run once per configuration file. The database dumps are
|
||||
|
|
|
|||
|
|
@ -138,16 +138,22 @@ def parse_configuration(config_filename, schema_filename, overrides=None, resolv
|
|||
return config, config_paths, logs
|
||||
|
||||
|
||||
def normalize_repository_path(repository):
|
||||
def normalize_repository_path(repository, base=None):
|
||||
'''
|
||||
Given a repository path, return the absolute path of it (for local repositories).
|
||||
Optionally, use a base path for resolving relative paths, e.g. to the configured working directory.
|
||||
'''
|
||||
# A colon in the repository could mean that it's either a file:// URL or a remote repository.
|
||||
# If it's a remote repository, we don't want to normalize it. If it's a file:// URL, we do.
|
||||
if ':' not in repository:
|
||||
return os.path.abspath(repository)
|
||||
return (
|
||||
os.path.abspath(os.path.join(base, repository)) if base else os.path.abspath(repository)
|
||||
)
|
||||
elif repository.startswith('file://'):
|
||||
return os.path.abspath(repository.partition('file://')[-1])
|
||||
local_path = repository.partition('file://')[-1]
|
||||
return (
|
||||
os.path.abspath(os.path.join(base, local_path)) if base else os.path.abspath(local_path)
|
||||
)
|
||||
else:
|
||||
return repository
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ import logging
|
|||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import borgmatic.execute
|
||||
import borgmatic.logger
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -44,54 +46,184 @@ def make_environment(current_environment, sys_module=sys):
|
|||
return environment
|
||||
|
||||
|
||||
def execute_hook(commands, umask, config_filename, description, dry_run, **context):
|
||||
def filter_hooks(command_hooks, before=None, after=None, hook_name=None, action_names=None):
|
||||
'''
|
||||
Given a list of hook commands to execute, a umask to execute with (or None), a config filename,
|
||||
a hook description, and whether this is a dry run, run the given commands. Or, don't run them
|
||||
if this is a dry run.
|
||||
Given a sequence of command hook dicts from configuration and one or more filters (before name,
|
||||
after name, calling hook name, or a sequence of action names), filter down the command hooks to
|
||||
just the ones that match the given filters.
|
||||
'''
|
||||
return tuple(
|
||||
hook_config
|
||||
for hook_config in command_hooks or ()
|
||||
for config_action_names in (hook_config.get('when'),)
|
||||
if before is None or hook_config.get('before') == before
|
||||
if after is None or hook_config.get('after') == after
|
||||
if action_names is None
|
||||
or config_action_names is None
|
||||
or set(config_action_names or ()).intersection(set(action_names))
|
||||
)
|
||||
|
||||
|
||||
def execute_hooks(command_hooks, umask, working_directory, dry_run, **context):
|
||||
'''
|
||||
Given a sequence of command hook dicts from configuration, a umask to execute with (or None), a
|
||||
working directory to execute with, and whether this is a dry run, run the commands for each
|
||||
hook. Or don't run them if this is a dry run.
|
||||
|
||||
The context contains optional values interpolated by name into the hook commands.
|
||||
|
||||
Raise ValueError if the umask cannot be parsed.
|
||||
Raise ValueError if the umask cannot be parsed or a hook is invalid.
|
||||
Raise subprocesses.CalledProcessError if an error occurs in a hook.
|
||||
'''
|
||||
if not commands:
|
||||
logger.debug(f'No commands to run for {description} hook')
|
||||
return
|
||||
borgmatic.logger.add_custom_log_levels()
|
||||
|
||||
dry_run_label = ' (dry run; not actually running hooks)' if dry_run else ''
|
||||
|
||||
context['configuration_filename'] = config_filename
|
||||
commands = [interpolate_context(description, command, context) for command in commands]
|
||||
for hook_config in command_hooks:
|
||||
commands = hook_config.get('run')
|
||||
|
||||
if len(commands) == 1:
|
||||
logger.info(f'Running command for {description} hook{dry_run_label}')
|
||||
else:
|
||||
logger.info(
|
||||
f'Running {len(commands)} commands for {description} hook{dry_run_label}',
|
||||
)
|
||||
if 'before' in hook_config:
|
||||
description = f'before {hook_config.get("before")}'
|
||||
elif 'after' in hook_config:
|
||||
description = f'after {hook_config.get("after")}'
|
||||
else:
|
||||
raise ValueError(f'Invalid hook configuration: {hook_config}')
|
||||
|
||||
if umask:
|
||||
parsed_umask = int(str(umask), 8)
|
||||
logger.debug(f'Set hook umask to {oct(parsed_umask)}')
|
||||
original_umask = os.umask(parsed_umask)
|
||||
else:
|
||||
original_umask = None
|
||||
if not commands:
|
||||
logger.debug(f'No commands to run for {description} hook')
|
||||
continue
|
||||
|
||||
try:
|
||||
for command in commands:
|
||||
if dry_run:
|
||||
continue
|
||||
commands = [interpolate_context(description, command, context) for command in commands]
|
||||
|
||||
borgmatic.execute.execute_command(
|
||||
[command],
|
||||
output_log_level=(logging.ERROR if description == 'on-error' else logging.WARNING),
|
||||
shell=True,
|
||||
environment=make_environment(os.environ),
|
||||
if len(commands) == 1:
|
||||
logger.info(f'Running {description} command hook{dry_run_label}')
|
||||
else:
|
||||
logger.info(
|
||||
f'Running {len(commands)} commands for {description} hook{dry_run_label}',
|
||||
)
|
||||
finally:
|
||||
if original_umask:
|
||||
os.umask(original_umask)
|
||||
|
||||
if umask:
|
||||
parsed_umask = int(str(umask), 8)
|
||||
logger.debug(f'Setting hook umask to {oct(parsed_umask)}')
|
||||
original_umask = os.umask(parsed_umask)
|
||||
else:
|
||||
original_umask = None
|
||||
|
||||
try:
|
||||
for command in commands:
|
||||
if dry_run:
|
||||
continue
|
||||
|
||||
borgmatic.execute.execute_command(
|
||||
[command],
|
||||
output_log_level=(
|
||||
logging.ERROR if hook_config.get('after') == 'error' else logging.ANSWER
|
||||
),
|
||||
shell=True,
|
||||
environment=make_environment(os.environ),
|
||||
working_directory=working_directory,
|
||||
)
|
||||
finally:
|
||||
if original_umask:
|
||||
os.umask(original_umask)
|
||||
|
||||
|
||||
class Before_after_hooks:
|
||||
'''
|
||||
A Python context manager for executing command hooks both before and after the wrapped code.
|
||||
|
||||
Example use as a context manager:
|
||||
|
||||
with borgmatic.hooks.command.Before_after_hooks(
|
||||
command_hooks=config.get('commands'),
|
||||
before_after='do_stuff',
|
||||
umask=config.get('umask'),
|
||||
dry_run=dry_run,
|
||||
hook_name='myhook',
|
||||
):
|
||||
do()
|
||||
some()
|
||||
stuff()
|
||||
|
||||
With that context manager in place, "before" command hooks execute before the wrapped code runs,
|
||||
and "after" command hooks execute after the wrapped code completes.
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
command_hooks,
|
||||
before_after,
|
||||
umask,
|
||||
working_directory,
|
||||
dry_run,
|
||||
hook_name=None,
|
||||
action_names=None,
|
||||
**context,
|
||||
):
|
||||
'''
|
||||
Given a sequence of command hook configuration dicts, the before/after name, a umask to run
|
||||
commands with, a working directory to run commands with, a dry run flag, the name of the
|
||||
calling hook, a sequence of action names, and any context for the executed commands, save
|
||||
those data points for use below.
|
||||
'''
|
||||
self.command_hooks = command_hooks
|
||||
self.before_after = before_after
|
||||
self.umask = umask
|
||||
self.working_directory = working_directory
|
||||
self.dry_run = dry_run
|
||||
self.hook_name = hook_name
|
||||
self.action_names = action_names
|
||||
self.context = context
|
||||
|
||||
def __enter__(self):
|
||||
'''
|
||||
Run the configured "before" command hooks that match the initialized data points.
|
||||
'''
|
||||
try:
|
||||
execute_hooks(
|
||||
borgmatic.hooks.command.filter_hooks(
|
||||
self.command_hooks,
|
||||
before=self.before_after,
|
||||
hook_name=self.hook_name,
|
||||
action_names=self.action_names,
|
||||
),
|
||||
self.umask,
|
||||
self.working_directory,
|
||||
self.dry_run,
|
||||
**self.context,
|
||||
)
|
||||
except (OSError, subprocess.CalledProcessError) as error:
|
||||
if considered_soft_failure(error):
|
||||
return
|
||||
|
||||
# Trigger the after hook manually, since raising here will prevent it from being run
|
||||
# otherwise.
|
||||
self.__exit__(None, None, None)
|
||||
|
||||
raise ValueError(f'Error running before {self.before_after} hook: {error}')
|
||||
|
||||
def __exit__(self, exception_type, exception, traceback):
|
||||
'''
|
||||
Run the configured "after" command hooks that match the initialized data points.
|
||||
'''
|
||||
try:
|
||||
execute_hooks(
|
||||
borgmatic.hooks.command.filter_hooks(
|
||||
self.command_hooks,
|
||||
after=self.before_after,
|
||||
hook_name=self.hook_name,
|
||||
action_names=self.action_names,
|
||||
),
|
||||
self.umask,
|
||||
self.working_directory,
|
||||
self.dry_run,
|
||||
**self.context,
|
||||
)
|
||||
except (OSError, subprocess.CalledProcessError) as error:
|
||||
if considered_soft_failure(error):
|
||||
return
|
||||
|
||||
raise ValueError(f'Error running after {self.before_after} hook: {error}')
|
||||
|
||||
|
||||
def considered_soft_failure(error):
|
||||
|
|
|
|||
|
|
@ -19,9 +19,11 @@ def load_credential(hook_config, config, credential_parameters):
|
|||
|
||||
raise ValueError(f'Cannot load invalid credential: "{name}"')
|
||||
|
||||
expanded_credential_path = os.path.expanduser(credential_path)
|
||||
|
||||
try:
|
||||
with open(
|
||||
os.path.join(config.get('working_directory', ''), credential_path)
|
||||
os.path.join(config.get('working_directory', ''), expanded_credential_path)
|
||||
) as credential_file:
|
||||
return credential_file.read().rstrip(os.linesep)
|
||||
except (FileNotFoundError, OSError) as error:
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@ def load_credential(hook_config, config, credential_parameters):
|
|||
f'Cannot load credential with invalid KeePassXC database path and attribute name: "{path_and_name}"'
|
||||
)
|
||||
|
||||
if not os.path.exists(database_path):
|
||||
expanded_database_path = os.path.expanduser(database_path)
|
||||
|
||||
if not os.path.exists(expanded_database_path):
|
||||
raise ValueError(
|
||||
f'Cannot load credential because KeePassXC database path does not exist: {database_path}'
|
||||
)
|
||||
|
|
@ -36,7 +38,7 @@ def load_credential(hook_config, config, credential_parameters):
|
|||
'--show-protected',
|
||||
'--attributes',
|
||||
'Password',
|
||||
database_path,
|
||||
expanded_database_path,
|
||||
attribute_name,
|
||||
)
|
||||
).rstrip(os.linesep)
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ def dump_data_sources(
|
|||
logger.info(f'Dumping MongoDB databases{dry_run_label}')
|
||||
|
||||
processes = []
|
||||
|
||||
for database in databases:
|
||||
name = database['name']
|
||||
dump_filename = dump.make_data_source_dump_filename(
|
||||
|
|
@ -113,14 +114,17 @@ def make_password_config_file(password):
|
|||
|
||||
def build_dump_command(database, config, dump_filename, dump_format):
|
||||
'''
|
||||
Return the mongodump command from a single database configuration.
|
||||
Return the custom mongodump_command from a single database configuration.
|
||||
'''
|
||||
all_databases = database['name'] == 'all'
|
||||
|
||||
password = borgmatic.hooks.credential.parse.resolve_credential(database.get('password'), config)
|
||||
|
||||
dump_command = tuple(
|
||||
shlex.quote(part) for part in shlex.split(database.get('mongodump_command') or 'mongodump')
|
||||
)
|
||||
return (
|
||||
('mongodump',)
|
||||
dump_command
|
||||
+ (('--out', shlex.quote(dump_filename)) if dump_format == 'directory' else ())
|
||||
+ (('--host', shlex.quote(database['hostname'])) if 'hostname' in database else ())
|
||||
+ (('--port', shlex.quote(str(database['port']))) if 'port' in database else ())
|
||||
|
|
@ -229,7 +233,7 @@ def restore_data_source_dump(
|
|||
|
||||
def build_restore_command(extract_process, database, config, dump_filename, connection_params):
|
||||
'''
|
||||
Return the mongorestore command from a single database configuration.
|
||||
Return the custom mongorestore_command from a single database configuration.
|
||||
'''
|
||||
hostname = connection_params['hostname'] or database.get(
|
||||
'restore_hostname', database.get('hostname')
|
||||
|
|
@ -250,7 +254,10 @@ def build_restore_command(extract_process, database, config, dump_filename, conn
|
|||
config,
|
||||
)
|
||||
|
||||
command = ['mongorestore']
|
||||
command = list(
|
||||
shlex.quote(part)
|
||||
for part in shlex.split(database.get('mongorestore_command') or 'mongorestore')
|
||||
)
|
||||
if extract_process:
|
||||
command.append('--archive')
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -71,13 +71,16 @@ def dump_data_sources(
|
|||
)
|
||||
continue
|
||||
|
||||
command = (
|
||||
'sqlite3',
|
||||
sqlite_command = tuple(
|
||||
shlex.quote(part) for part in shlex.split(database.get('sqlite_command') or 'sqlite3')
|
||||
)
|
||||
command = sqlite_command + (
|
||||
shlex.quote(database_path),
|
||||
'.dump',
|
||||
'>',
|
||||
shlex.quote(dump_filename),
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f'Dumping SQLite database at {database_path} to {dump_filename}{dry_run_label}'
|
||||
)
|
||||
|
|
@ -160,11 +163,11 @@ def restore_data_source_dump(
|
|||
except FileNotFoundError: # pragma: no cover
|
||||
pass
|
||||
|
||||
restore_command = (
|
||||
'sqlite3',
|
||||
database_path,
|
||||
sqlite_restore_command = tuple(
|
||||
shlex.quote(part)
|
||||
for part in shlex.split(data_source.get('sqlite_restore_command') or 'sqlite3')
|
||||
)
|
||||
|
||||
restore_command = sqlite_restore_command + (shlex.quote(database_path),)
|
||||
# Don't give Borg local path so as to error on warnings, as "borg extract" only gives a warning
|
||||
# if the restore paths don't exist in the archive.
|
||||
execute_command_with_processes(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import importlib
|
|||
import logging
|
||||
import pkgutil
|
||||
|
||||
import borgmatic.hooks.command
|
||||
import borgmatic.hooks.credential
|
||||
import borgmatic.hooks.data_source
|
||||
import borgmatic.hooks.monitoring
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
|||
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'Ignoring unsupported monitoring {state.name.lower()} in Cronhub hook')
|
||||
logger.debug(f'Ignoring unsupported monitoring state {state.name.lower()} in Cronhub hook')
|
||||
return
|
||||
|
||||
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
|||
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'Ignoring unsupported monitoring {state.name.lower()} in Cronitor hook')
|
||||
logger.debug(f'Ignoring unsupported monitoring state {state.name.lower()} in Cronitor hook')
|
||||
return
|
||||
|
||||
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
|||
'''
|
||||
if state != monitor.State.FAIL:
|
||||
logger.debug(
|
||||
f'Ignoring unsupported monitoring {state.name.lower()} in PagerDuty hook',
|
||||
f'Ignoring unsupported monitoring state {state.name.lower()} in PagerDuty hook',
|
||||
)
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@ class Log_prefix:
|
|||
self.original_prefix = get_log_prefix()
|
||||
set_log_prefix(self.prefix)
|
||||
|
||||
def __exit__(self, exception, value, traceback):
|
||||
def __exit__(self, exception_type, exception, traceback):
|
||||
'''
|
||||
Restore any original prefix.
|
||||
'''
|
||||
|
|
|
|||
|
|
@ -165,6 +165,7 @@ ul {
|
|||
}
|
||||
li {
|
||||
padding: .25em 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
li ul {
|
||||
list-style-type: disc;
|
||||
|
|
|
|||
|
|
@ -8,18 +8,111 @@ eleventyNavigation:
|
|||
## Preparation and cleanup hooks
|
||||
|
||||
If you find yourself performing preparation tasks before your backup runs or
|
||||
cleanup work afterwards, borgmatic command hooks may be of interest. These are
|
||||
custom shell commands you can configure borgmatic to execute at various points
|
||||
as it runs.
|
||||
doing cleanup work afterwards, borgmatic command hooks may be of interest. These
|
||||
are custom shell commands you can configure borgmatic to execute at various
|
||||
points as it runs.
|
||||
|
||||
But if you're looking to backup a database, it's probably easier to use the
|
||||
(But if you're looking to backup a database, it's probably easier to use the
|
||||
[database backup
|
||||
feature](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/)
|
||||
instead.
|
||||
instead.)
|
||||
|
||||
You can specify `before_backup` hooks to perform preparation steps before
|
||||
<span class="minilink minilink-addedin">New in version 2.0.0 (not yet
|
||||
released)</span> Command hooks are now configured via a list of `commands:` in
|
||||
your borgmatic configuration file. For example:
|
||||
|
||||
```yaml
|
||||
commands:
|
||||
- before: action
|
||||
when: [create]
|
||||
run:
|
||||
- echo "Before create!"
|
||||
- after: action
|
||||
when:
|
||||
- create
|
||||
- prune
|
||||
run:
|
||||
- echo "After create or prune!"
|
||||
- after: error
|
||||
run:
|
||||
- echo "Something went wrong!"
|
||||
```
|
||||
|
||||
If you're coming from an older version of borgmatic, there is tooling to help
|
||||
you [upgrade your
|
||||
configuration](https://torsion.org/borgmatic/docs/how-to/upgrade/#upgrading-your-configuration)
|
||||
to this new command hook format.
|
||||
|
||||
Note that if a `run:` command contains a special YAML character such as a colon,
|
||||
you may need to quote the entire string (or use a [multiline
|
||||
string](https://yaml-multiline.info/)) to avoid an error:
|
||||
|
||||
```yaml
|
||||
commands:
|
||||
- before: action
|
||||
when: [create]
|
||||
run:
|
||||
- "echo Backup: start"
|
||||
```
|
||||
|
||||
Each command in the `commands:` list has the following options:
|
||||
|
||||
* `before` or `after`: Name for the point in borgmatic's execution that the commands should be run before or after, one of:
|
||||
* `action` runs before each action for each repository. This replaces the deprecated `before_create`, `after_prune`, etc.
|
||||
* `repository` runs before or after all actions for each repository. This replaces the deprecated `before_actions` and `after_actions`.
|
||||
* `configuration` runs before or after all actions and repositories in the current configuration file.
|
||||
* `everything` runs before or after all configuration files. Errors here do not trigger `error` hooks or the `fail` state in monitoring hooks. This replaces the deprecated `before_everything` and `after_everything`.
|
||||
* `error` runs after an error occurs—and it's only available for `after`. This replaces the deprecated `on_error` hook.
|
||||
* `when`: Only trigger the hook when borgmatic is run with particular actions (`create`, `prune`, etc.) listed here. Defaults to running for all actions.
|
||||
* `run`: List of one or more shell commands or scripts to run when this command hook is triggered.
|
||||
|
||||
An `after` command hook runs even if an error occurs in the corresponding
|
||||
`before` hook or between those two hooks. This allows you to perform cleanup
|
||||
steps that correspond to `before` preparation commands—even when something goes
|
||||
wrong. This is a departure from the way that the deprecated `after_*` hooks
|
||||
worked in borgmatic prior to version 2.0.0.
|
||||
|
||||
Additionally, when command hooks run, they respect the `working_directory`
|
||||
option if it is configured, meaning that the hook commands are run in that
|
||||
directory.
|
||||
|
||||
|
||||
### Order of execution
|
||||
|
||||
Here's a way of visualizing how all of these command hooks slot into borgmatic's
|
||||
execution.
|
||||
|
||||
Let's say you've got a borgmatic configuration file with a configured
|
||||
repository. And suppose you configure several command hooks and then run
|
||||
borgmatic for the `create` and `prune` actions. Here's the order of execution:
|
||||
|
||||
* Run `before: everything` hooks (from all configuration files).
|
||||
* Run `before: configuration` hooks (from the first configuration file).
|
||||
* Run `before: repository` hooks (for the first repository).
|
||||
* Run `before: action` hooks for `create`.
|
||||
* Actually run the `create` action (e.g. `borg create`).
|
||||
* Run `after: action` hooks for `create`.
|
||||
* Run `before: action` hooks for `prune`.
|
||||
* Actually run the `prune` action (e.g. `borg prune`).
|
||||
* Run `after: action` hooks for `prune`.
|
||||
* Run `after: repository` hooks (for the first repository).
|
||||
* Run `after: configuration` hooks (from the first configuration file).
|
||||
* Run `after: everything` hooks (from all configuration files).
|
||||
|
||||
This same order of execution extends to multiple repositories and/or
|
||||
configuration files.
|
||||
|
||||
|
||||
### Deprecated command hooks
|
||||
|
||||
<span class="minilink minilink-addedin">Prior to version 2.0.0</span> The
|
||||
command hooks worked a little differently. In these older versions of borgmatic,
|
||||
you can specify `before_backup` hooks to perform preparation steps before
|
||||
running backups and specify `after_backup` hooks to perform cleanup steps
|
||||
afterwards. Here's an example:
|
||||
afterwards. These deprecated command hooks still work in version 2.0.0+,
|
||||
although see below about a few semantic differences starting in that version.
|
||||
|
||||
Here's an example of these deprecated hooks:
|
||||
|
||||
```yaml
|
||||
before_backup:
|
||||
|
|
@ -44,6 +137,15 @@ instance, `before_prune` runs before a `prune` action for a repository, while
|
|||
<span class="minilink minilink-addedin">Prior to version 1.8.0</span> Put
|
||||
these options in the `hooks:` section of your configuration.
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 2.0.0</span> An `after_*`
|
||||
command hook runs even if an error occurs in the corresponding `before_*` hook
|
||||
or between those two hooks. This allows you to perform cleanup steps that
|
||||
correspond to `before_*` preparation commands—even when something goes wrong.
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 2.0.0</span> When command
|
||||
hooks run, they respect the `working_directory` option if it is configured,
|
||||
meaning that the hook commands are run in that directory.
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.7.0</span> The
|
||||
`before_actions` and `after_actions` hooks run before/after all the actions
|
||||
(like `create`, `prune`, etc.) for each repository. These hooks are a good
|
||||
|
|
@ -58,49 +160,13 @@ but not if an error occurs in a previous hook or in the backups themselves.
|
|||
(Prior to borgmatic 1.6.0, these hooks instead ran once per configuration file
|
||||
rather than once per repository.)
|
||||
|
||||
|
||||
## Variable interpolation
|
||||
|
||||
The before and after action hooks support interpolating particular runtime
|
||||
variables into the hook command. Here's an example that assumes you provide a
|
||||
separate shell script:
|
||||
|
||||
```yaml
|
||||
after_prune:
|
||||
- record-prune.sh "{configuration_filename}" "{repository}"
|
||||
```
|
||||
|
||||
<span class="minilink minilink-addedin">Prior to version 1.8.0</span> Put
|
||||
this option in the `hooks:` section of your configuration.
|
||||
|
||||
In this example, when the hook is triggered, borgmatic interpolates runtime
|
||||
values into the hook command: the borgmatic configuration filename and the
|
||||
paths of the current Borg repository. Here's the full set of supported
|
||||
variables you can use here:
|
||||
|
||||
* `configuration_filename`: borgmatic configuration filename in which the
|
||||
hook was defined
|
||||
* `log_file`
|
||||
<span class="minilink minilink-addedin">New in version 1.7.12</span>:
|
||||
path of the borgmatic log file, only set when the `--log-file` flag is used
|
||||
* `repository`: path of the current repository as configured in the current
|
||||
borgmatic configuration file
|
||||
* `repository_label` <span class="minilink minilink-addedin">New in version
|
||||
1.8.12</span>: label of the current repository as configured in the current
|
||||
borgmatic configuration file
|
||||
|
||||
Note that you can also interpolate in [arbitrary environment
|
||||
variables](https://torsion.org/borgmatic/docs/how-to/provide-your-passwords/).
|
||||
|
||||
|
||||
## Global hooks
|
||||
|
||||
You can also use `before_everything` and `after_everything` hooks to perform
|
||||
global setup or cleanup:
|
||||
|
||||
```yaml
|
||||
before_everything:
|
||||
- set-up-stuff-globally
|
||||
|
||||
after_everything:
|
||||
- clean-up-stuff-globally
|
||||
```
|
||||
|
|
@ -118,13 +184,102 @@ but only if there is a `create` action. It runs even if an error occurs during
|
|||
a backup or a backup hook, but not if an error occurs during a
|
||||
`before_everything` hook.
|
||||
|
||||
`on_error` hooks run when an error occurs, but only if there is a `create`,
|
||||
`prune`, `compact`, or `check` action. For instance, 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:
|
||||
|
||||
## Error hooks
|
||||
```yaml
|
||||
on_error:
|
||||
- echo "Error while creating a backup or running a backup hook."
|
||||
```
|
||||
|
||||
borgmatic also runs `on_error` hooks if an error occurs, either when creating
|
||||
a backup or running a backup hook. See the [monitoring and alerting
|
||||
documentation](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)
|
||||
for more information.
|
||||
<span class="minilink minilink-addedin">Prior to version 1.8.0</span> Put
|
||||
this option in the `hooks:` section of your configuration.
|
||||
|
||||
The `on_error` hook supports interpolating particular runtime variables into
|
||||
the hook command. Here's an example that assumes you provide a separate shell
|
||||
script to handle the alerting:
|
||||
|
||||
```yaml
|
||||
on_error:
|
||||
- send-text-message.sh
|
||||
```
|
||||
|
||||
borgmatic does not run `on_error` hooks if an error occurs within a
|
||||
`before_everything` or `after_everything` hook.
|
||||
|
||||
|
||||
## Variable interpolation
|
||||
|
||||
The command action hooks support interpolating particular runtime variables into
|
||||
the commands that are run. Here's are a couple examples that assume you provide
|
||||
separate shell scripts:
|
||||
|
||||
```yaml
|
||||
commands:
|
||||
- after: action
|
||||
when: [prune]
|
||||
run:
|
||||
- record-prune.sh {configuration_filename} {repository}
|
||||
- after: error
|
||||
when: [create]
|
||||
run:
|
||||
- send-text-message.sh {configuration_filename} {repository}
|
||||
```
|
||||
|
||||
In this example, when the hook is triggered, borgmatic interpolates runtime
|
||||
values into each hook command: the borgmatic configuration filename and the
|
||||
paths of the current Borg repository.
|
||||
|
||||
Here's the full set of supported variables you can use here:
|
||||
|
||||
* `configuration_filename`: borgmatic configuration filename in which the
|
||||
hook was defined
|
||||
* `log_file`
|
||||
<span class="minilink minilink-addedin">New in version 1.7.12</span>:
|
||||
path of the borgmatic log file, only set when the `--log-file` flag is used
|
||||
* `repository`: path of the current repository as configured in the current
|
||||
borgmatic configuration file, if applicable to the current hook
|
||||
* `repository_label` <span class="minilink minilink-addedin">New in version
|
||||
1.8.12</span>: label of the current repository as configured in the current
|
||||
borgmatic configuration file, if applicable to the current hook
|
||||
* `error`: the error message itself, only applies to `error` hooks
|
||||
* `output`: output of the command that failed, only applies to `error` hooks
|
||||
(may be blank if an error occurred without running a command)
|
||||
|
||||
Not all command hooks support all variables. For instance, the `everything` and
|
||||
`configuration` hooks don't support repository variables because those hooks
|
||||
don't run in the context of a single repository. But the deprecated command
|
||||
hooks (`before_backup`, `on_error`, etc.) do generally support variable
|
||||
interpolation.
|
||||
|
||||
borgmatic automatically escapes these interpolated values to prevent shell
|
||||
injection attacks. One implication is that you shouldn't wrap the interpolated
|
||||
values in your own quotes, as that will interfere with the quoting performed by
|
||||
borgmatic and result in your command receiving incorrect arguments. For
|
||||
instance, this won't work:
|
||||
|
||||
```yaml
|
||||
commands:
|
||||
- after: error
|
||||
run:
|
||||
# Don't do this! It won't work, as the {error} value is already quoted.
|
||||
- send-text-message.sh "Uh oh: {error}"
|
||||
```
|
||||
|
||||
Do this instead:
|
||||
|
||||
```yaml
|
||||
commands:
|
||||
- after: error
|
||||
run:
|
||||
- send-text-message.sh {error}
|
||||
```
|
||||
|
||||
Note that you can also interpolate [arbitrary environment
|
||||
variables](https://torsion.org/borgmatic/docs/how-to/provide-your-passwords/).
|
||||
|
||||
|
||||
## Hook output
|
||||
|
|
|
|||
|
|
@ -29,17 +29,14 @@ concept of "soft failure" come in.
|
|||
|
||||
This feature leverages [borgmatic command
|
||||
hooks](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/),
|
||||
so first familiarize yourself with them. The idea is that you write a simple
|
||||
test in the form of a borgmatic hook to see if backups should proceed or not.
|
||||
so familiarize yourself with them first. The idea is that you write a simple
|
||||
test in the form of a borgmatic command hook to see if backups should proceed or
|
||||
not.
|
||||
|
||||
The way the test works is that if any of your hook commands return a special
|
||||
exit status of 75, that indicates to borgmatic that it's a temporary failure,
|
||||
and borgmatic should skip all subsequent actions for the current repository.
|
||||
|
||||
<span class="minilink minilink-addedin">Prior to version 1.9.0</span> Soft
|
||||
failures skipped subsequent actions for *all* repositories in the
|
||||
configuration file, rather than just for the current repository.
|
||||
|
||||
If you return any status besides 75, then it's a standard success or error.
|
||||
(Zero is success; anything else other than 75 is an error).
|
||||
|
||||
|
|
@ -62,33 +59,37 @@ these options in the `location:` section of your configuration.
|
|||
<span class="minilink minilink-addedin">Prior to version 1.7.10</span> Omit
|
||||
the `path:` portion of the `repositories` list.
|
||||
|
||||
Then, write a `before_backup` hook in that same configuration file that uses
|
||||
the external `findmnt` utility to see whether the drive is mounted before
|
||||
proceeding.
|
||||
Then, make a command hook in that same configuration file that uses the external
|
||||
`findmnt` utility to see whether the drive is mounted before proceeding.
|
||||
|
||||
```yaml
|
||||
before_backup:
|
||||
commands:
|
||||
- before: repository
|
||||
run:
|
||||
- findmnt /mnt/removable > /dev/null || exit 75
|
||||
```
|
||||
|
||||
<span class="minilink minilink-addedin">Prior to version 2.0.0</span> Use the
|
||||
deprecated `before_actions` hook instead:
|
||||
|
||||
```yaml
|
||||
before_actions:
|
||||
- findmnt /mnt/removable > /dev/null || exit 75
|
||||
```
|
||||
|
||||
<span class="minilink minilink-addedin">Prior to version 1.8.0</span> Put this
|
||||
option in the `hooks:` section of your configuration.
|
||||
|
||||
<span class="minilink minilink-addedin">Prior to version 1.7.0</span> Use
|
||||
`before_create` or similar instead of `before_actions`, which was introduced in
|
||||
borgmatic 1.7.0.
|
||||
|
||||
What this does is check if the `findmnt` command errors when probing for a
|
||||
particular mount point. If it does error, then it returns exit code 75 to
|
||||
borgmatic. borgmatic logs the soft failure, skips all further actions for the
|
||||
current repository, and proceeds onward to any other repositories and/or
|
||||
configuration files you may have.
|
||||
|
||||
If you'd prefer not to use a separate configuration file, and you'd rather
|
||||
have multiple repositories in a single configuration file, you can make your
|
||||
`before_backup` soft failure test [vary by
|
||||
repository](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/#variable-interpolation).
|
||||
That might require calling out to a separate script though.
|
||||
|
||||
Note that `before_backup` only runs on the `create` action. See below about
|
||||
optionally using `before_actions` instead.
|
||||
|
||||
You can imagine a similar check for the sometimes-online server case:
|
||||
|
||||
```yaml
|
||||
|
|
@ -98,50 +99,50 @@ source_directories:
|
|||
repositories:
|
||||
- path: ssh://me@buddys-server.org/./backup.borg
|
||||
|
||||
before_backup:
|
||||
- ping -q -c 1 buddys-server.org > /dev/null || exit 75
|
||||
commands:
|
||||
- before: repository
|
||||
run:
|
||||
- ping -q -c 1 buddys-server.org > /dev/null || exit 75
|
||||
```
|
||||
|
||||
Or to only run backups if the battery level is high enough:
|
||||
|
||||
```yaml
|
||||
before_backup:
|
||||
- is_battery_percent_at_least.sh 25
|
||||
commands:
|
||||
- before: repository
|
||||
run:
|
||||
- is_battery_percent_at_least.sh 25
|
||||
```
|
||||
|
||||
(Writing the battery script is left as an exercise to the reader.)
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.7.0</span> The
|
||||
`before_actions` and `after_actions` hooks run before/after all the actions
|
||||
(like `create`, `prune`, etc.) for each repository. So if you'd like your soft
|
||||
failure command hook to run regardless of action, consider using
|
||||
`before_actions` instead of `before_backup`.
|
||||
Writing the battery script is left as an exercise to the reader.
|
||||
|
||||
|
||||
## Caveats and details
|
||||
|
||||
There are some caveats you should be aware of with this feature.
|
||||
|
||||
* You'll generally want to put a soft failure command in the `before_backup`
|
||||
* You'll generally want to put a soft failure command in a `before` command
|
||||
hook, so as to gate whether the backup action occurs. While a soft failure is
|
||||
also supported in the `after_backup` hook, returning a soft failure there
|
||||
also supported in an `after` command hook, returning a soft failure there
|
||||
won't prevent any actions from occurring, because they've already occurred!
|
||||
Similarly, you can return a soft failure from an `on_error` hook, but at
|
||||
Similarly, you can return a soft failure from an `error` command hook, but at
|
||||
that point it's too late to prevent the error.
|
||||
* Returning a soft failure does prevent further commands in the same hook from
|
||||
executing. So, like a standard error, it is an "early out". Unlike a standard
|
||||
executing. So, like a standard error, it is an "early out." Unlike a standard
|
||||
error, borgmatic does not display it in angry red text or consider it a
|
||||
failure.
|
||||
* Any given soft failure only applies to the a single borgmatic repository
|
||||
(as of borgmatic 1.9.0). So if you have other repositories you don't want
|
||||
soft-failed, then make your soft fail test [vary by
|
||||
repository](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/#variable-interpolation)—or
|
||||
put anything that you don't want soft-failed (like always-online cloud
|
||||
backups) in separate configuration files from your soft-failing
|
||||
repositories.
|
||||
* <span class="minilink minilink-addedin">New in version 1.9.0</span> Soft
|
||||
failures in `action` or `before_*` command hooks only skip the current
|
||||
repository rather than all repositories in a configuration file.
|
||||
* If you're writing a soft failure script that you want to vary based on the
|
||||
current repository, for instance so you can have multiple repositories in a
|
||||
single configuration file, have a look at [command hook variable
|
||||
interpolation](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/#variable-interpolation).
|
||||
And there's always still the option of putting anything that you don't want
|
||||
soft-failed (like always-online cloud backups) in separate configuration
|
||||
files from your soft-failing repositories.
|
||||
* The soft failure doesn't have to test anything related to a repository. You
|
||||
can even perform a test to make sure that individual source directories are
|
||||
mounted and available. Use your imagination!
|
||||
* The soft failure feature also works for before/after hooks for other
|
||||
actions as well. But it is not implemented for `before_everything` or
|
||||
`after_everything`.
|
||||
can even perform a test that individual source directories are mounted and
|
||||
available. Use your imagination!
|
||||
* Soft failures are not currently implemented for `everything`,
|
||||
`before_everything`, or `after_everything` command hooks.
|
||||
|
|
|
|||
|
|
@ -193,14 +193,14 @@ mysql_databases:
|
|||
|
||||
### Containers
|
||||
|
||||
If your database is running within a container and borgmatic is too, no
|
||||
If your database server is running within a container and borgmatic is too, no
|
||||
problem—configure borgmatic to connect to the container's name on its exposed
|
||||
port. For instance:
|
||||
|
||||
```yaml
|
||||
postgresql_databases:
|
||||
- name: users
|
||||
hostname: your-database-container-name
|
||||
hostname: your-database-server-container-name
|
||||
port: 5433
|
||||
username: postgres
|
||||
password: trustsome1
|
||||
|
|
@ -210,21 +210,22 @@ postgresql_databases:
|
|||
these options in the `hooks:` section of your configuration.
|
||||
|
||||
But what if borgmatic is running on the host? You can still connect to a
|
||||
database container if its ports are properly exposed to the host. For
|
||||
database server container if its ports are properly exposed to the host. For
|
||||
instance, when running the database container, you can specify `--publish
|
||||
127.0.0.1:5433:5432` so that it exposes the container's port 5432 to port 5433
|
||||
on the host (only reachable on localhost, in this case). Or the same thing
|
||||
with Docker Compose:
|
||||
on the host (only reachable on localhost, in this case). Or the same thing with
|
||||
Docker Compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
your-database-container-name:
|
||||
your-database-server-container-name:
|
||||
image: postgres
|
||||
ports:
|
||||
- 127.0.0.1:5433:5432
|
||||
```
|
||||
|
||||
And then you can connect to the database from borgmatic running on the host:
|
||||
And then you can configure borgmatic running on the host to connect to the
|
||||
database:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
|
|
@ -240,9 +241,9 @@ Alter the ports in these examples to suit your particular database system.
|
|||
|
||||
Normally, borgmatic dumps a database by running a database dump command (e.g.
|
||||
`pg_dump`) on the host or wherever borgmatic is running, and this command
|
||||
connects to your containerized database via the given `hostname` and `port`.
|
||||
But if you don't have any database dump commands installed on your host and
|
||||
you'd rather use the commands inside your database container itself, borgmatic
|
||||
connects to your containerized database via the given `hostname` and `port`. But
|
||||
if you don't have any database dump commands installed on your host and you'd
|
||||
rather use the commands inside your running database container itself, borgmatic
|
||||
supports that too. For that, configure borgmatic to `exec` into your container
|
||||
to run the dump command.
|
||||
|
||||
|
|
@ -259,9 +260,10 @@ hooks:
|
|||
pg_dump_command: docker exec my_pg_container pg_dump
|
||||
```
|
||||
|
||||
... where `my_pg_container` is the name of your database container. In this
|
||||
example, you'd also need to set the `pg_restore_command` and `psql_command`
|
||||
options.
|
||||
... where `my_pg_container` is the name of your running database container.
|
||||
Running `pg_dump` this way takes advantage of the localhost "trust"
|
||||
authentication within that container. In this example, you'd also need to set
|
||||
the `pg_restore_command` and `psql_command` options.
|
||||
|
||||
If you choose to use the `pg_dump` command within the container, and you're
|
||||
using the `directory` format in particular, you'll also need to mount the
|
||||
|
|
@ -280,6 +282,24 @@ services:
|
|||
- /run/user/1000:/run/user/1000
|
||||
```
|
||||
|
||||
Another variation: If you're running borgmatic on the host but want to spin up a
|
||||
temporary `pg_dump` container whenever borgmatic dumps a database, for
|
||||
instance to make use of a `pg_dump` version not present on the host, try
|
||||
something like this:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
postgresql_databases:
|
||||
- name: users
|
||||
hostname: your-database-hostname
|
||||
username: postgres
|
||||
password: trustsome1
|
||||
pg_dump_command: docker run --rm --env PGPASSWORD postgres:17-alpine pg_dump
|
||||
```
|
||||
|
||||
The `--env PGPASSWORD` is necessary here for borgmatic to provide your database
|
||||
password to the temporary `pg_dump` container.
|
||||
|
||||
Similar command override options are available for (some of) the other
|
||||
supported database types as well. See the [configuration
|
||||
reference](https://torsion.org/borgmatic/docs/reference/configuration/) for
|
||||
|
|
|
|||
|
|
@ -14,140 +14,55 @@ and alerting comes in.
|
|||
|
||||
There are several different ways you can monitor your backups and find out
|
||||
whether they're succeeding. Which of these you choose to do is up to you and
|
||||
your particular infrastructure.
|
||||
your particular infrastructure:
|
||||
|
||||
### Job runner alerts
|
||||
|
||||
The easiest place to start is with failure alerts from the [scheduled job
|
||||
runner](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#autopilot)
|
||||
(cron, systemd, etc.) that's running borgmatic. But note that if the job
|
||||
doesn't even get scheduled (e.g. due to the job runner not running), you
|
||||
probably won't get an alert at all! Still, this is a decent first line of
|
||||
defense, especially when combined with some of the other approaches below.
|
||||
|
||||
### Commands run on error
|
||||
|
||||
The `on_error` hook allows you to run an arbitrary command or script when
|
||||
borgmatic itself encounters an error running your backups. So for instance,
|
||||
you can run a script to send yourself a text message alert. But note that if
|
||||
borgmatic doesn't actually run, this alert won't fire. See [error
|
||||
hooks](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#error-hooks)
|
||||
below for how to configure this.
|
||||
|
||||
### Third-party monitoring services
|
||||
|
||||
borgmatic integrates with these monitoring services and libraries, pinging
|
||||
them as backups happen:
|
||||
|
||||
* [Apprise](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#apprise-hook)
|
||||
* [Cronhub](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronhub-hook)
|
||||
* [Cronitor](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronitor-hook)
|
||||
* [Grafana Loki](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#loki-hook)
|
||||
* [Healthchecks](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#healthchecks-hook)
|
||||
* [ntfy](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#ntfy-hook)
|
||||
* [PagerDuty](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook)
|
||||
* [Pushover](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pushover-hook)
|
||||
* [Sentry](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#sentry-hook)
|
||||
* [Uptime Kuma](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#uptime-kuma-hook)
|
||||
* [Zabbix](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#zabbix-hook)
|
||||
|
||||
The idea is that you'll receive an alert when something goes wrong or when the
|
||||
service doesn't hear from borgmatic for a configured interval (if supported).
|
||||
See the documentation links above for configuration information.
|
||||
|
||||
While these services and libraries offer different features, you probably only
|
||||
need to use one of them at most.
|
||||
|
||||
|
||||
### Third-party monitoring software
|
||||
|
||||
You can use traditional monitoring software to consume borgmatic JSON output
|
||||
and track when the last successful backup occurred. See [scripting
|
||||
borgmatic](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#scripting-borgmatic)
|
||||
below for how to configure this.
|
||||
|
||||
### Borg hosting providers
|
||||
|
||||
Most [Borg hosting
|
||||
providers](https://torsion.org/borgmatic/#hosting-providers) include
|
||||
monitoring and alerting as part of their offering. This gives you a dashboard
|
||||
to check on all of your backups, and can alert you if the service doesn't hear
|
||||
from borgmatic for a configured interval.
|
||||
|
||||
### Consistency checks
|
||||
|
||||
While not strictly part of monitoring, if you want confidence that your
|
||||
backups are not only running but are restorable as well, you can configure
|
||||
particular [consistency
|
||||
checks](https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups/#consistency-check-configuration)
|
||||
or even script full [extract
|
||||
tests](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/).
|
||||
|
||||
|
||||
## Error hooks
|
||||
|
||||
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:
|
||||
|
||||
```yaml
|
||||
on_error:
|
||||
- echo "Error while creating a backup or running a backup hook."
|
||||
```
|
||||
|
||||
<span class="minilink minilink-addedin">Prior to version 1.8.0</span> Put
|
||||
this option in the `hooks:` section of your configuration.
|
||||
|
||||
The `on_error` hook supports interpolating particular runtime variables into
|
||||
the hook command. Here's an example that assumes you provide a separate shell
|
||||
script to handle the alerting:
|
||||
|
||||
```yaml
|
||||
on_error:
|
||||
- send-text-message.sh {configuration_filename} {repository}
|
||||
```
|
||||
|
||||
In this example, when the error occurs, borgmatic interpolates runtime values
|
||||
into the hook command: the borgmatic configuration filename and the path of
|
||||
the repository. Here's the full set of supported variables you can use here:
|
||||
|
||||
* `configuration_filename`: borgmatic configuration filename in which the
|
||||
error occurred
|
||||
* `repository`: path of the repository in which the error occurred (may be
|
||||
blank if the error occurs in a hook)
|
||||
* `error`: the error message itself
|
||||
* `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 `create`, `prune`,
|
||||
`compact`, or `check` actions/hooks in which an error occurs and not other
|
||||
actions. borgmatic does not run `on_error` hooks if an error occurs within a
|
||||
`before_everything` or `after_everything` hook. For more about hooks, see the
|
||||
[borgmatic hooks
|
||||
documentation](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/),
|
||||
especially the security information.
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.8.7</span> borgmatic
|
||||
automatically escapes these interpolated values to prevent shell injection
|
||||
attacks. One implication of this change is that you shouldn't wrap the
|
||||
interpolated values in your own quotes, as that will interfere with the
|
||||
quoting performed by borgmatic and result in your command receiving incorrect
|
||||
arguments. For instance, this won't work:
|
||||
|
||||
|
||||
```yaml
|
||||
on_error:
|
||||
# Don't do this! It won't work, as the {error} value is already quoted.
|
||||
- send-text-message.sh "Uh oh: {error}"
|
||||
```
|
||||
|
||||
Do this instead:
|
||||
|
||||
```yaml
|
||||
on_error:
|
||||
- send-text-message.sh {error}
|
||||
```
|
||||
* **Job runner alerts**: The easiest place to start is with failure alerts from
|
||||
the [scheduled job
|
||||
runner](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#autopilot)
|
||||
(cron, systemd, etc.) that's running borgmatic. But note that if the job
|
||||
doesn't even get scheduled (e.g. due to the job runner not running), you
|
||||
probably won't get an alert at all! Still, this is a decent first line of
|
||||
defense, especially when combined with some of the other approaches below.
|
||||
* **Third-party monitoring services:** borgmatic integrates with these monitoring
|
||||
services and libraries, pinging them as backups happen. The idea is that
|
||||
you'll receive an alert when something goes wrong or when the service doesn't
|
||||
hear from borgmatic for a configured interval (if supported). While these
|
||||
services and libraries offer different features, you probably only need to
|
||||
use one of them at most. See these documentation links for configuration
|
||||
information:
|
||||
* [Apprise](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#apprise-hook)
|
||||
* [Cronhub](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronhub-hook)
|
||||
* [Cronitor](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronitor-hook)
|
||||
* [Grafana Loki](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#loki-hook)
|
||||
* [Healthchecks](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#healthchecks-hook)
|
||||
* [ntfy](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#ntfy-hook)
|
||||
* [PagerDuty](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook)
|
||||
* [Pushover](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pushover-hook)
|
||||
* [Sentry](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#sentry-hook)
|
||||
* [Uptime Kuma](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#uptime-kuma-hook)
|
||||
* [Zabbix](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#zabbix-hook)
|
||||
* **Third-party monitoring software:** You can use traditional monitoring
|
||||
software to consume borgmatic JSON output and track when the last successful
|
||||
backup occurred. See [scripting
|
||||
borgmatic](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#scripting-borgmatic)
|
||||
below for how to configure this.
|
||||
* **Borg hosting providers:** Some [Borg hosting
|
||||
providers](https://torsion.org/borgmatic/#hosting-providers) include
|
||||
monitoring and alerting as part of their offering. This gives you a dashboard
|
||||
to check on all of your backups, and can alert you if the service doesn't
|
||||
hear from borgmatic for a configured interval.
|
||||
* **Consistency checks:** While not strictly part of monitoring, if you want
|
||||
confidence that your backups are not only running but are restorable as well,
|
||||
you can configure particular [consistency
|
||||
checks](https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups/#consistency-check-configuration)
|
||||
or even script full [extract
|
||||
tests](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/).
|
||||
* **Commands run on error:** borgmatic's command hooks support running
|
||||
arbitrary commands or scripts when borgmatic itself encounters an error
|
||||
running your backups. So for instance, you can run a script to send yourself
|
||||
a text message alert. But note that if borgmatic doesn't actually run, this
|
||||
alert won't fire. See the [documentation on command hooks](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/)
|
||||
for details.
|
||||
|
||||
|
||||
## Healthchecks hook
|
||||
|
|
|
|||
|
|
@ -311,7 +311,6 @@ Then, from the directory where you downloaded it:
|
|||
|
||||
```bash
|
||||
sudo mv borgmatic /etc/cron.d/borgmatic
|
||||
sudo chmod +x /etc/cron.d/borgmatic
|
||||
```
|
||||
|
||||
If borgmatic is installed at a different location than
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "borgmatic"
|
||||
version = "1.9.14"
|
||||
version = "2.0.0dev0"
|
||||
authors = [
|
||||
{ name="Dan Helfman", email="witten@torsion.org" },
|
||||
]
|
||||
|
|
|
|||
|
|
@ -16,6 +16,116 @@ def test_insert_newline_before_comment_does_not_raise():
|
|||
module.insert_newline_before_comment(config, field_name)
|
||||
|
||||
|
||||
def test_schema_to_sample_configuration_comments_out_non_default_options():
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'properties': dict(
|
||||
[
|
||||
('field1', {'example': 'Example 1'}),
|
||||
('field2', {'example': 'Example 2'}),
|
||||
('source_directories', {'example': 'Example 3'}),
|
||||
]
|
||||
),
|
||||
}
|
||||
|
||||
config = module.schema_to_sample_configuration(schema)
|
||||
|
||||
assert config == dict(
|
||||
[
|
||||
('field1', 'Example 1'),
|
||||
('field2', 'Example 2'),
|
||||
('source_directories', 'Example 3'),
|
||||
]
|
||||
)
|
||||
assert 'COMMENT_OUT' in config.ca.items['field1'][1][-1]._value
|
||||
assert 'COMMENT_OUT' in config.ca.items['field2'][1][-1]._value
|
||||
assert 'source_directories' not in config.ca.items
|
||||
|
||||
|
||||
def test_schema_to_sample_configuration_comments_out_non_source_config_options():
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'properties': dict(
|
||||
[
|
||||
('field1', {'example': 'Example 1'}),
|
||||
('field2', {'example': 'Example 2'}),
|
||||
('field3', {'example': 'Example 3'}),
|
||||
]
|
||||
),
|
||||
}
|
||||
source_config = {'field3': 'value'}
|
||||
|
||||
config = module.schema_to_sample_configuration(schema, source_config)
|
||||
|
||||
assert config == dict(
|
||||
[
|
||||
('field1', 'Example 1'),
|
||||
('field2', 'Example 2'),
|
||||
('field3', 'Example 3'),
|
||||
]
|
||||
)
|
||||
assert 'COMMENT_OUT' in config.ca.items['field1'][1][-1]._value
|
||||
assert 'COMMENT_OUT' in config.ca.items['field2'][1][-1]._value
|
||||
assert 'field3' not in config.ca.items
|
||||
|
||||
|
||||
def test_schema_to_sample_configuration_comments_out_non_default_options_in_sequence_of_maps():
|
||||
schema = {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'properties': dict(
|
||||
[
|
||||
('field1', {'example': 'Example 1'}),
|
||||
('field2', {'example': 'Example 2'}),
|
||||
('source_directories', {'example': 'Example 3'}),
|
||||
]
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
config = module.schema_to_sample_configuration(schema)
|
||||
|
||||
assert config == [
|
||||
dict(
|
||||
[('field1', 'Example 1'), ('field2', 'Example 2'), ('source_directories', 'Example 3')]
|
||||
)
|
||||
]
|
||||
|
||||
# The first field in a sequence does not get commented.
|
||||
assert 'field1' not in config[0].ca.items
|
||||
assert 'COMMENT_OUT' in config[0].ca.items['field2'][1][-1]._value
|
||||
assert 'source_directories' not in config[0].ca.items
|
||||
|
||||
|
||||
def test_schema_to_sample_configuration_comments_out_non_source_config_options_in_sequence_of_maps():
|
||||
schema = {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'properties': dict(
|
||||
[
|
||||
('field1', {'example': 'Example 1'}),
|
||||
('field2', {'example': 'Example 2'}),
|
||||
('field3', {'example': 'Example 3'}),
|
||||
]
|
||||
),
|
||||
},
|
||||
}
|
||||
source_config = [{'field3': 'value'}]
|
||||
|
||||
config = module.schema_to_sample_configuration(schema, source_config)
|
||||
|
||||
assert config == [
|
||||
dict([('field1', 'Example 1'), ('field2', 'Example 2'), ('field3', 'Example 3')])
|
||||
]
|
||||
|
||||
# The first field in a sequence does not get commented.
|
||||
assert 'field1' not in config[0].ca.items
|
||||
assert 'COMMENT_OUT' in config[0].ca.items['field2'][1][-1]._value
|
||||
assert 'field3' not in config[0].ca.items
|
||||
|
||||
|
||||
def test_comment_out_line_skips_blank_line():
|
||||
line = ' \n'
|
||||
|
||||
|
|
@ -152,7 +262,7 @@ def test_add_comments_to_configuration_sequence_of_maps_without_description_does
|
|||
module.add_comments_to_configuration_sequence(config, schema)
|
||||
|
||||
|
||||
def test_add_comments_to_configuration_object_does_not_raise():
|
||||
def test_add_comments_to_configuration_comments_out_non_default_options():
|
||||
# Ensure that it can deal with fields both in the schema and missing from the schema.
|
||||
config = module.ruamel.yaml.comments.CommentedMap([('foo', 33), ('bar', 44), ('baz', 55)])
|
||||
schema = {
|
||||
|
|
@ -162,44 +272,43 @@ def test_add_comments_to_configuration_object_does_not_raise():
|
|||
|
||||
module.add_comments_to_configuration_object(config, schema)
|
||||
|
||||
assert 'COMMENT_OUT' in config.ca.items['foo'][1][-1]._value
|
||||
assert 'COMMENT_OUT' in config.ca.items['bar'][1][-1]._value
|
||||
assert 'baz' not in config.ca.items
|
||||
|
||||
def test_add_comments_to_configuration_object_with_skip_first_does_not_raise():
|
||||
config = module.ruamel.yaml.comments.CommentedMap([('foo', 33)])
|
||||
schema = {'type': 'object', 'properties': {'foo': {'description': 'Foo'}}}
|
||||
|
||||
def test_add_comments_to_configuration_comments_out_non_source_config_options():
|
||||
# Ensure that it can deal with fields both in the schema and missing from the schema.
|
||||
config = module.ruamel.yaml.comments.CommentedMap(
|
||||
[('repositories', 33), ('bar', 44), ('baz', 55)]
|
||||
)
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'repositories': {'description': 'repositories'},
|
||||
'bar': {'description': 'Bar'},
|
||||
},
|
||||
}
|
||||
|
||||
module.add_comments_to_configuration_object(config, schema)
|
||||
|
||||
assert 'repositories' in config.ca.items
|
||||
assert 'COMMENT_OUT' in config.ca.items['bar'][1][-1]._value
|
||||
assert 'baz' not in config.ca.items
|
||||
|
||||
|
||||
def test_add_comments_to_configuration_object_with_skip_first_does_not_comment_out_first_option():
|
||||
config = module.ruamel.yaml.comments.CommentedMap([('foo', 33), ('bar', 44), ('baz', 55)])
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'properties': {'foo': {'description': 'Foo'}, 'bar': {'description': 'Bar'}},
|
||||
}
|
||||
|
||||
module.add_comments_to_configuration_object(config, schema, skip_first=True)
|
||||
|
||||
|
||||
def test_remove_commented_out_sentinel_keeps_other_comments():
|
||||
field_name = 'foo'
|
||||
config = module.ruamel.yaml.comments.CommentedMap([(field_name, 33)])
|
||||
config.yaml_set_comment_before_after_key(key=field_name, before='Actual comment.\nCOMMENT_OUT')
|
||||
|
||||
module.remove_commented_out_sentinel(config, field_name)
|
||||
|
||||
comments = config.ca.items[field_name][module.RUAMEL_YAML_COMMENTS_INDEX]
|
||||
assert len(comments) == 1
|
||||
assert comments[0].value == '# Actual comment.\n'
|
||||
|
||||
|
||||
def test_remove_commented_out_sentinel_without_sentinel_keeps_other_comments():
|
||||
field_name = 'foo'
|
||||
config = module.ruamel.yaml.comments.CommentedMap([(field_name, 33)])
|
||||
config.yaml_set_comment_before_after_key(key=field_name, before='Actual comment.')
|
||||
|
||||
module.remove_commented_out_sentinel(config, field_name)
|
||||
|
||||
comments = config.ca.items[field_name][module.RUAMEL_YAML_COMMENTS_INDEX]
|
||||
assert len(comments) == 1
|
||||
assert comments[0].value == '# Actual comment.\n'
|
||||
|
||||
|
||||
def test_remove_commented_out_sentinel_on_unknown_field_does_not_raise():
|
||||
field_name = 'foo'
|
||||
config = module.ruamel.yaml.comments.CommentedMap([(field_name, 33)])
|
||||
config.yaml_set_comment_before_after_key(key=field_name, before='Actual comment.')
|
||||
|
||||
module.remove_commented_out_sentinel(config, 'unknown')
|
||||
assert 'foo' not in config.ca.items
|
||||
assert 'COMMENT_OUT' in config.ca.items['bar'][1][-1]._value
|
||||
assert 'baz' not in config.ca.items
|
||||
|
||||
|
||||
def test_generate_sample_configuration_does_not_raise():
|
||||
|
|
|
|||
|
|
@ -14,7 +14,13 @@ def test_schema_line_length_stays_under_limit():
|
|||
assert len(line.rstrip('\n')) <= MAXIMUM_LINE_LENGTH
|
||||
|
||||
|
||||
ACTIONS_MODULE_NAMES_TO_OMIT = {'arguments', 'change_passphrase', 'export_key', 'json'}
|
||||
ACTIONS_MODULE_NAMES_TO_OMIT = {
|
||||
'arguments',
|
||||
'change_passphrase',
|
||||
'export_key',
|
||||
'import_key',
|
||||
'json',
|
||||
}
|
||||
ACTIONS_MODULE_NAMES_TO_ADD = {'key', 'umount'}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1405,7 +1405,6 @@ def test_run_check_checks_archives_for_configured_repository():
|
|||
flexmock(module).should_receive('make_check_time_path')
|
||||
flexmock(module).should_receive('write_check_time')
|
||||
flexmock(module.borgmatic.borg.extract).should_receive('extract_last_archive_dry_run').never()
|
||||
flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
|
||||
check_arguments = flexmock(
|
||||
repository=None,
|
||||
progress=flexmock(),
|
||||
|
|
@ -1419,7 +1418,6 @@ def test_run_check_checks_archives_for_configured_repository():
|
|||
config_filename='test.yaml',
|
||||
repository={'path': 'repo'},
|
||||
config={'repositories': ['repo']},
|
||||
hook_context={},
|
||||
local_borg_version=None,
|
||||
check_arguments=check_arguments,
|
||||
global_arguments=global_arguments,
|
||||
|
|
@ -1441,7 +1439,6 @@ def test_run_check_runs_configured_extract_check():
|
|||
flexmock(module.borgmatic.borg.extract).should_receive('extract_last_archive_dry_run').once()
|
||||
flexmock(module).should_receive('make_check_time_path')
|
||||
flexmock(module).should_receive('write_check_time')
|
||||
flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
|
||||
check_arguments = flexmock(
|
||||
repository=None,
|
||||
progress=flexmock(),
|
||||
|
|
@ -1455,7 +1452,6 @@ def test_run_check_runs_configured_extract_check():
|
|||
config_filename='test.yaml',
|
||||
repository={'path': 'repo'},
|
||||
config={'repositories': ['repo']},
|
||||
hook_context={},
|
||||
local_borg_version=None,
|
||||
check_arguments=check_arguments,
|
||||
global_arguments=global_arguments,
|
||||
|
|
@ -1480,7 +1476,6 @@ def test_run_check_runs_configured_spot_check():
|
|||
flexmock(module.borgmatic.actions.check).should_receive('spot_check').once()
|
||||
flexmock(module).should_receive('make_check_time_path')
|
||||
flexmock(module).should_receive('write_check_time')
|
||||
flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
|
||||
check_arguments = flexmock(
|
||||
repository=None,
|
||||
progress=flexmock(),
|
||||
|
|
@ -1494,7 +1489,6 @@ def test_run_check_runs_configured_spot_check():
|
|||
config_filename='test.yaml',
|
||||
repository={'path': 'repo'},
|
||||
config={'repositories': ['repo']},
|
||||
hook_context={},
|
||||
local_borg_version=None,
|
||||
check_arguments=check_arguments,
|
||||
global_arguments=global_arguments,
|
||||
|
|
@ -1516,7 +1510,6 @@ def test_run_check_without_checks_runs_nothing_except_hooks():
|
|||
flexmock(module).should_receive('make_check_time_path')
|
||||
flexmock(module).should_receive('write_check_time').never()
|
||||
flexmock(module.borgmatic.borg.extract).should_receive('extract_last_archive_dry_run').never()
|
||||
flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
|
||||
check_arguments = flexmock(
|
||||
repository=None,
|
||||
progress=flexmock(),
|
||||
|
|
@ -1530,7 +1523,6 @@ def test_run_check_without_checks_runs_nothing_except_hooks():
|
|||
config_filename='test.yaml',
|
||||
repository={'path': 'repo'},
|
||||
config={'repositories': ['repo']},
|
||||
hook_context={},
|
||||
local_borg_version=None,
|
||||
check_arguments=check_arguments,
|
||||
global_arguments=global_arguments,
|
||||
|
|
@ -1569,7 +1561,6 @@ def test_run_check_checks_archives_in_selected_repository():
|
|||
config_filename='test.yaml',
|
||||
repository={'path': 'repo'},
|
||||
config={'repositories': ['repo']},
|
||||
hook_context={},
|
||||
local_borg_version=None,
|
||||
check_arguments=check_arguments,
|
||||
global_arguments=global_arguments,
|
||||
|
|
@ -1597,7 +1588,6 @@ def test_run_check_bails_if_repository_does_not_match():
|
|||
config_filename='test.yaml',
|
||||
repository={'path': 'repo'},
|
||||
config={'repositories': ['repo']},
|
||||
hook_context={},
|
||||
local_borg_version=None,
|
||||
check_arguments=check_arguments,
|
||||
global_arguments=global_arguments,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ def test_compact_actions_calls_hooks_for_configured_repository():
|
|||
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
|
||||
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(
|
||||
repository=None, progress=flexmock(), cleanup_commits=flexmock(), threshold=flexmock()
|
||||
)
|
||||
|
|
@ -18,7 +17,6 @@ def test_compact_actions_calls_hooks_for_configured_repository():
|
|||
config_filename='test.yaml',
|
||||
repository={'path': 'repo'},
|
||||
config={},
|
||||
hook_context={},
|
||||
local_borg_version=None,
|
||||
compact_arguments=compact_arguments,
|
||||
global_arguments=global_arguments,
|
||||
|
|
@ -44,7 +42,6 @@ def test_compact_runs_with_selected_repository():
|
|||
config_filename='test.yaml',
|
||||
repository={'path': 'repo'},
|
||||
config={},
|
||||
hook_context={},
|
||||
local_borg_version=None,
|
||||
compact_arguments=compact_arguments,
|
||||
global_arguments=global_arguments,
|
||||
|
|
@ -70,7 +67,6 @@ def test_compact_bails_if_repository_does_not_match():
|
|||
config_filename='test.yaml',
|
||||
repository={'path': 'repo'},
|
||||
config={},
|
||||
hook_context={},
|
||||
local_borg_version=None,
|
||||
compact_arguments=compact_arguments,
|
||||
global_arguments=global_arguments,
|
||||
|
|
|
|||
|
|
@ -433,7 +433,6 @@ def test_run_create_executes_and_calls_hooks_for_configured_repository():
|
|||
flexmock()
|
||||
)
|
||||
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'
|
||||
|
|
@ -456,7 +455,6 @@ def test_run_create_executes_and_calls_hooks_for_configured_repository():
|
|||
repository={'path': 'repo'},
|
||||
config={},
|
||||
config_paths=['/tmp/test.yaml'],
|
||||
hook_context={},
|
||||
local_borg_version=None,
|
||||
create_arguments=create_arguments,
|
||||
global_arguments=global_arguments,
|
||||
|
|
@ -476,7 +474,6 @@ def test_run_create_runs_with_selected_repository():
|
|||
flexmock()
|
||||
)
|
||||
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'
|
||||
|
|
@ -499,7 +496,6 @@ def test_run_create_runs_with_selected_repository():
|
|||
repository={'path': 'repo'},
|
||||
config={},
|
||||
config_paths=['/tmp/test.yaml'],
|
||||
hook_context={},
|
||||
local_borg_version=None,
|
||||
create_arguments=create_arguments,
|
||||
global_arguments=global_arguments,
|
||||
|
|
@ -532,7 +528,6 @@ def test_run_create_bails_if_repository_does_not_match():
|
|||
repository='repo',
|
||||
config={},
|
||||
config_paths=['/tmp/test.yaml'],
|
||||
hook_context={},
|
||||
local_borg_version=None,
|
||||
create_arguments=create_arguments,
|
||||
global_arguments=global_arguments,
|
||||
|
|
@ -556,7 +551,6 @@ def test_run_create_produces_json():
|
|||
)
|
||||
parsed_json = flexmock()
|
||||
flexmock(module.borgmatic.actions.json).should_receive('parse_json').and_return(parsed_json)
|
||||
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'
|
||||
|
|
@ -579,7 +573,6 @@ def test_run_create_produces_json():
|
|||
repository={'path': 'repo'},
|
||||
config={},
|
||||
config_paths=['/tmp/test.yaml'],
|
||||
hook_context={},
|
||||
local_borg_version=None,
|
||||
create_arguments=create_arguments,
|
||||
global_arguments=global_arguments,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ def test_run_extract_calls_hooks():
|
|||
flexmock(module.logger).answer = lambda message: None
|
||||
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
|
||||
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive')
|
||||
flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
|
||||
extract_arguments = flexmock(
|
||||
paths=flexmock(),
|
||||
progress=flexmock(),
|
||||
|
|
@ -22,7 +21,6 @@ def test_run_extract_calls_hooks():
|
|||
config_filename='test.yaml',
|
||||
repository={'path': 'repo'},
|
||||
config={'repositories': ['repo']},
|
||||
hook_context={},
|
||||
local_borg_version=None,
|
||||
extract_arguments=extract_arguments,
|
||||
global_arguments=global_arguments,
|
||||
|
|
|
|||
20
tests/unit/actions/test_import_key.py
Normal file
20
tests/unit/actions/test_import_key.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.actions import import_key as module
|
||||
|
||||
|
||||
def test_run_import_key_does_not_raise():
|
||||
flexmock(module.logger).answer = lambda message: None
|
||||
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
|
||||
flexmock(module.borgmatic.borg.import_key).should_receive('import_key')
|
||||
import_arguments = flexmock(repository=flexmock())
|
||||
|
||||
module.run_import_key(
|
||||
repository={'path': 'repo'},
|
||||
config={},
|
||||
local_borg_version=None,
|
||||
import_arguments=import_arguments,
|
||||
global_arguments=flexmock(),
|
||||
local_path=None,
|
||||
remote_path=None,
|
||||
)
|
||||
|
|
@ -7,7 +7,6 @@ def test_run_prune_calls_hooks_for_configured_repository():
|
|||
flexmock(module.logger).answer = lambda message: None
|
||||
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(repository=None, stats=flexmock(), list_archives=flexmock())
|
||||
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
|
||||
|
||||
|
|
@ -15,7 +14,6 @@ def test_run_prune_calls_hooks_for_configured_repository():
|
|||
config_filename='test.yaml',
|
||||
repository={'path': 'repo'},
|
||||
config={},
|
||||
hook_context={},
|
||||
local_borg_version=None,
|
||||
prune_arguments=prune_arguments,
|
||||
global_arguments=global_arguments,
|
||||
|
|
@ -38,7 +36,6 @@ def test_run_prune_runs_with_selected_repository():
|
|||
config_filename='test.yaml',
|
||||
repository={'path': 'repo'},
|
||||
config={},
|
||||
hook_context={},
|
||||
local_borg_version=None,
|
||||
prune_arguments=prune_arguments,
|
||||
global_arguments=global_arguments,
|
||||
|
|
@ -61,7 +58,6 @@ def test_run_prune_bails_if_repository_does_not_match():
|
|||
config_filename='test.yaml',
|
||||
repository='repo',
|
||||
config={},
|
||||
hook_context={},
|
||||
local_borg_version=None,
|
||||
prune_arguments=prune_arguments,
|
||||
global_arguments=global_arguments,
|
||||
|
|
|
|||
|
|
@ -710,7 +710,7 @@ def test_extract_archive_uses_configured_working_directory_in_repo_path_and_dest
|
|||
)
|
||||
flexmock(module.borgmatic.config.validate).should_receive(
|
||||
'normalize_repository_path'
|
||||
).with_args('/working/dir/repo').and_return('/working/dir/repo').once()
|
||||
).with_args('repo', '/working/dir').and_return('/working/dir/repo').once()
|
||||
|
||||
module.extract_archive(
|
||||
dry_run=False,
|
||||
|
|
@ -733,7 +733,7 @@ def test_extract_archive_uses_configured_working_directory_in_repo_path_when_des
|
|||
)
|
||||
flexmock(module.borgmatic.config.validate).should_receive(
|
||||
'normalize_repository_path'
|
||||
).with_args('/working/dir/repo').and_return('/working/dir/repo').once()
|
||||
).with_args('repo', '/working/dir').and_return('/working/dir/repo').once()
|
||||
|
||||
module.extract_archive(
|
||||
dry_run=False,
|
||||
|
|
|
|||
279
tests/unit/borg/test_import_key.py
Normal file
279
tests/unit/borg/test_import_key.py
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
import logging
|
||||
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.borg import import_key as module
|
||||
|
||||
from ..test_verbosity import insert_logging_mock
|
||||
|
||||
|
||||
def insert_execute_command_mock(
|
||||
command, input_file=module.DO_NOT_CAPTURE, working_directory=None, borg_exit_codes=None
|
||||
):
|
||||
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
|
||||
working_directory,
|
||||
)
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
command,
|
||||
input_file=input_file,
|
||||
output_log_level=module.logging.INFO,
|
||||
environment=None,
|
||||
working_directory=working_directory,
|
||||
borg_local_path=command[0],
|
||||
borg_exit_codes=borg_exit_codes,
|
||||
).once()
|
||||
|
||||
|
||||
def test_import_key_calls_borg_with_required_flags():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'import', 'repo'))
|
||||
|
||||
module.import_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
import_arguments=flexmock(paper=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_import_key_calls_borg_with_local_path():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg1', 'key', 'import', 'repo'))
|
||||
|
||||
module.import_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
import_arguments=flexmock(paper=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
local_path='borg1',
|
||||
)
|
||||
|
||||
|
||||
def test_import_key_calls_borg_using_exit_codes():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
borg_exit_codes = flexmock()
|
||||
insert_execute_command_mock(('borg', 'key', 'import', 'repo'), borg_exit_codes=borg_exit_codes)
|
||||
|
||||
module.import_key(
|
||||
repository_path='repo',
|
||||
config={'borg_exit_codes': borg_exit_codes},
|
||||
local_borg_version='1.2.3',
|
||||
import_arguments=flexmock(paper=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_import_key_calls_borg_with_remote_path_flags():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'import', '--remote-path', 'borg1', 'repo'))
|
||||
|
||||
module.import_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
import_arguments=flexmock(paper=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
remote_path='borg1',
|
||||
)
|
||||
|
||||
|
||||
def test_import_key_calls_borg_with_umask_flags():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'import', '--umask', '0770', 'repo'))
|
||||
|
||||
module.import_key(
|
||||
repository_path='repo',
|
||||
config={'umask': '0770'},
|
||||
local_borg_version='1.2.3',
|
||||
import_arguments=flexmock(paper=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_import_key_calls_borg_with_log_json_flags():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'import', '--log-json', 'repo'))
|
||||
|
||||
module.import_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
import_arguments=flexmock(paper=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=True),
|
||||
)
|
||||
|
||||
|
||||
def test_import_key_calls_borg_with_lock_wait_flags():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'import', '--lock-wait', '5', 'repo'))
|
||||
|
||||
module.import_key(
|
||||
repository_path='repo',
|
||||
config={'lock_wait': '5'},
|
||||
local_borg_version='1.2.3',
|
||||
import_arguments=flexmock(paper=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_import_key_with_log_info_calls_borg_with_info_parameter():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'import', '--info', 'repo'))
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
module.import_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
import_arguments=flexmock(paper=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_import_key_with_log_debug_calls_borg_with_debug_flags():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'import', '--debug', '--show-rc', 'repo'))
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
module.import_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
import_arguments=flexmock(paper=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_import_key_calls_borg_with_paper_flags():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(('--paper',))
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'import', '--paper', 'repo'))
|
||||
|
||||
module.import_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
import_arguments=flexmock(paper=True, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_import_key_calls_borg_with_path_argument():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').with_args('source').and_return(True)
|
||||
insert_execute_command_mock(('borg', 'key', 'import', 'repo', 'source'), input_file=None)
|
||||
|
||||
module.import_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
import_arguments=flexmock(paper=False, path='source'),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_import_key_with_non_existent_path_raises():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.import_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
import_arguments=flexmock(paper=False, path='source'),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_import_key_with_stdin_path_calls_borg_without_path_argument():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'import', 'repo'))
|
||||
|
||||
module.import_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
import_arguments=flexmock(paper=False, path='-'),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_import_key_with_dry_run_skips_borg_call():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
module.import_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
import_arguments=flexmock(paper=False, path=None),
|
||||
global_arguments=flexmock(dry_run=True, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_import_key_calls_borg_with_working_directory():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'import', 'repo'), working_directory='/working/dir')
|
||||
|
||||
module.import_key(
|
||||
repository_path='repo',
|
||||
config={'working_directory': '/working/dir'},
|
||||
local_borg_version='1.2.3',
|
||||
import_arguments=flexmock(paper=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_import_key_calls_borg_with_path_argument_and_working_directory():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').with_args('/working/dir/source').and_return(
|
||||
True
|
||||
).once()
|
||||
insert_execute_command_mock(
|
||||
('borg', 'key', 'import', 'repo', 'source'),
|
||||
input_file=None,
|
||||
working_directory='/working/dir',
|
||||
)
|
||||
|
||||
module.import_key(
|
||||
repository_path='repo',
|
||||
config={'working_directory': '/working/dir'},
|
||||
local_borg_version='1.2.3',
|
||||
import_arguments=flexmock(paper=False, path='source'),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
|
@ -210,6 +210,9 @@ def test_prune_archives_calls_borg_with_flags():
|
|||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.feature).should_receive('available').with_args(
|
||||
module.feature.Feature.NO_PRUNE_STATS, '1.2.3'
|
||||
).and_return(False)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('repo',), logging.INFO)
|
||||
|
||||
prune_arguments = flexmock(stats=False, list_archives=False)
|
||||
|
|
@ -228,6 +231,9 @@ def test_prune_archives_with_log_info_calls_borg_with_info_flag():
|
|||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.feature).should_receive('available').with_args(
|
||||
module.feature.Feature.NO_PRUNE_STATS, '1.2.3'
|
||||
).and_return(False)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--info', 'repo'), logging.INFO)
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
|
|
@ -247,6 +253,9 @@ def test_prune_archives_with_log_debug_calls_borg_with_debug_flag():
|
|||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.feature).should_receive('available').with_args(
|
||||
module.feature.Feature.NO_PRUNE_STATS, '1.2.3'
|
||||
).and_return(False)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--debug', '--show-rc', 'repo'), logging.INFO)
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
|
|
@ -266,6 +275,9 @@ def test_prune_archives_with_dry_run_calls_borg_with_dry_run_flag():
|
|||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.feature).should_receive('available').with_args(
|
||||
module.feature.Feature.NO_PRUNE_STATS, '1.2.3'
|
||||
).and_return(False)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--dry-run', 'repo'), logging.INFO)
|
||||
|
||||
prune_arguments = flexmock(stats=False, list_archives=False)
|
||||
|
|
@ -284,6 +296,9 @@ def test_prune_archives_with_local_path_calls_borg_via_local_path():
|
|||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.feature).should_receive('available').with_args(
|
||||
module.feature.Feature.NO_PRUNE_STATS, '1.2.3'
|
||||
).and_return(False)
|
||||
insert_execute_command_mock(('borg1',) + PRUNE_COMMAND[1:] + ('repo',), logging.INFO)
|
||||
|
||||
prune_arguments = flexmock(stats=False, list_archives=False)
|
||||
|
|
@ -303,6 +318,9 @@ def test_prune_archives_with_exit_codes_calls_borg_using_them():
|
|||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.feature).should_receive('available').with_args(
|
||||
module.feature.Feature.NO_PRUNE_STATS, '1.2.3'
|
||||
).and_return(False)
|
||||
borg_exit_codes = flexmock()
|
||||
insert_execute_command_mock(
|
||||
('borg',) + PRUNE_COMMAND[1:] + ('repo',),
|
||||
|
|
@ -326,6 +344,9 @@ def test_prune_archives_with_remote_path_calls_borg_with_remote_path_flags():
|
|||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.feature).should_receive('available').with_args(
|
||||
module.feature.Feature.NO_PRUNE_STATS, '1.2.3'
|
||||
).and_return(False)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--remote-path', 'borg1', 'repo'), logging.INFO)
|
||||
|
||||
prune_arguments = flexmock(stats=False, list_archives=False)
|
||||
|
|
@ -345,6 +366,9 @@ def test_prune_archives_with_stats_calls_borg_with_stats_flag_and_answer_output_
|
|||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.feature).should_receive('available').with_args(
|
||||
module.feature.Feature.NO_PRUNE_STATS, '1.2.3'
|
||||
).and_return(False)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--stats', 'repo'), module.borgmatic.logger.ANSWER)
|
||||
|
||||
prune_arguments = flexmock(stats=True, list_archives=False)
|
||||
|
|
@ -363,6 +387,9 @@ def test_prune_archives_with_files_calls_borg_with_list_flag_and_answer_output_l
|
|||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.feature).should_receive('available').with_args(
|
||||
module.feature.Feature.NO_PRUNE_STATS, '1.2.3'
|
||||
).and_return(False)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--list', 'repo'), module.borgmatic.logger.ANSWER)
|
||||
|
||||
prune_arguments = flexmock(stats=False, list_archives=True)
|
||||
|
|
@ -382,6 +409,9 @@ def test_prune_archives_with_umask_calls_borg_with_umask_flags():
|
|||
config = {'umask': '077'}
|
||||
flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.feature).should_receive('available').with_args(
|
||||
module.feature.Feature.NO_PRUNE_STATS, '1.2.3'
|
||||
).and_return(False)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--umask', '077', 'repo'), logging.INFO)
|
||||
|
||||
prune_arguments = flexmock(stats=False, list_archives=False)
|
||||
|
|
@ -400,6 +430,9 @@ def test_prune_archives_with_log_json_calls_borg_with_log_json_flag():
|
|||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.feature).should_receive('available').with_args(
|
||||
module.feature.Feature.NO_PRUNE_STATS, '1.2.3'
|
||||
).and_return(False)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--log-json', 'repo'), logging.INFO)
|
||||
|
||||
prune_arguments = flexmock(stats=False, list_archives=False)
|
||||
|
|
@ -419,6 +452,9 @@ def test_prune_archives_with_lock_wait_calls_borg_with_lock_wait_flags():
|
|||
config = {'lock_wait': 5}
|
||||
flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.feature).should_receive('available').with_args(
|
||||
module.feature.Feature.NO_PRUNE_STATS, '1.2.3'
|
||||
).and_return(False)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--lock-wait', '5', 'repo'), logging.INFO)
|
||||
|
||||
prune_arguments = flexmock(stats=False, list_archives=False)
|
||||
|
|
@ -437,6 +473,9 @@ def test_prune_archives_with_extra_borg_options_calls_borg_with_extra_options():
|
|||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.feature).should_receive('available').with_args(
|
||||
module.feature.Feature.NO_PRUNE_STATS, '1.2.3'
|
||||
).and_return(False)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--extra', '--options', 'repo'), logging.INFO)
|
||||
|
||||
prune_arguments = flexmock(stats=False, list_archives=False)
|
||||
|
|
@ -471,6 +510,9 @@ def test_prune_archives_with_date_based_matching_calls_borg_with_date_based_flag
|
|||
)
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||
flexmock(module.feature).should_receive('available').with_args(
|
||||
module.feature.Feature.NO_PRUNE_STATS, '1.2.3'
|
||||
).and_return(False)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
|
|
@ -521,6 +563,9 @@ def test_prune_archives_calls_borg_with_working_directory():
|
|||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.feature).should_receive('available').with_args(
|
||||
module.feature.Feature.NO_PRUNE_STATS, '1.2.3'
|
||||
).and_return(False)
|
||||
insert_execute_command_mock(
|
||||
PRUNE_COMMAND + ('repo',), logging.INFO, working_directory='/working/dir'
|
||||
)
|
||||
|
|
@ -534,3 +579,24 @@ def test_prune_archives_calls_borg_with_working_directory():
|
|||
global_arguments=flexmock(log_json=False),
|
||||
prune_arguments=prune_arguments,
|
||||
)
|
||||
|
||||
|
||||
def test_prune_archives_calls_borg_with_flags_and_when_feature_available():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.feature).should_receive('available').with_args(
|
||||
module.feature.Feature.NO_PRUNE_STATS, '2.0.0b10'
|
||||
).and_return(True)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('repo',), logging.ANSWER)
|
||||
|
||||
prune_arguments = flexmock(stats=True, list_archives=False)
|
||||
module.prune_archives(
|
||||
dry_run=False,
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='2.0.0b10',
|
||||
global_arguments=flexmock(log_json=False),
|
||||
prune_arguments=prune_arguments,
|
||||
)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,3 @@
|
|||
from collections import OrderedDict
|
||||
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
|
|
@ -9,7 +7,7 @@ from borgmatic.config import generate as module
|
|||
def test_get_properties_with_simple_object():
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'properties': OrderedDict(
|
||||
'properties': dict(
|
||||
[
|
||||
('field1', {'example': 'Example'}),
|
||||
]
|
||||
|
|
@ -19,12 +17,12 @@ def test_get_properties_with_simple_object():
|
|||
assert module.get_properties(schema) == schema['properties']
|
||||
|
||||
|
||||
def test_get_properties_merges_one_of_list_properties():
|
||||
def test_get_properties_merges_oneof_list_properties():
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'oneOf': [
|
||||
{
|
||||
'properties': OrderedDict(
|
||||
'properties': dict(
|
||||
[
|
||||
('field1', {'example': 'Example 1'}),
|
||||
('field2', {'example': 'Example 2'}),
|
||||
|
|
@ -32,7 +30,7 @@ def test_get_properties_merges_one_of_list_properties():
|
|||
),
|
||||
},
|
||||
{
|
||||
'properties': OrderedDict(
|
||||
'properties': dict(
|
||||
[
|
||||
('field2', {'example': 'Example 2'}),
|
||||
('field3', {'example': 'Example 3'}),
|
||||
|
|
@ -47,10 +45,45 @@ def test_get_properties_merges_one_of_list_properties():
|
|||
)
|
||||
|
||||
|
||||
def test_get_properties_interleaves_oneof_list_properties():
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'oneOf': [
|
||||
{
|
||||
'properties': dict(
|
||||
[
|
||||
('field1', {'example': 'Example 1'}),
|
||||
('field2', {'example': 'Example 2'}),
|
||||
('field3', {'example': 'Example 3'}),
|
||||
]
|
||||
),
|
||||
},
|
||||
{
|
||||
'properties': dict(
|
||||
[
|
||||
('field4', {'example': 'Example 4'}),
|
||||
('field5', {'example': 'Example 5'}),
|
||||
]
|
||||
),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
assert module.get_properties(schema) == dict(
|
||||
[
|
||||
('field1', {'example': 'Example 1'}),
|
||||
('field4', {'example': 'Example 4'}),
|
||||
('field2', {'example': 'Example 2'}),
|
||||
('field5', {'example': 'Example 5'}),
|
||||
('field3', {'example': 'Example 3'}),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_schema_to_sample_configuration_generates_config_map_with_examples():
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'properties': OrderedDict(
|
||||
'properties': dict(
|
||||
[
|
||||
('field1', {'example': 'Example 1'}),
|
||||
('field2', {'example': 'Example 2'}),
|
||||
|
|
@ -59,12 +92,12 @@ def test_schema_to_sample_configuration_generates_config_map_with_examples():
|
|||
),
|
||||
}
|
||||
flexmock(module).should_receive('get_properties').and_return(schema['properties'])
|
||||
flexmock(module.ruamel.yaml.comments).should_receive('CommentedMap').replace_with(OrderedDict)
|
||||
flexmock(module.ruamel.yaml.comments).should_receive('CommentedMap').replace_with(dict)
|
||||
flexmock(module).should_receive('add_comments_to_configuration_object')
|
||||
|
||||
config = module.schema_to_sample_configuration(schema)
|
||||
|
||||
assert config == OrderedDict(
|
||||
assert config == dict(
|
||||
[
|
||||
('field1', 'Example 1'),
|
||||
('field2', 'Example 2'),
|
||||
|
|
@ -88,7 +121,7 @@ def test_schema_to_sample_configuration_generates_config_sequence_of_maps_with_e
|
|||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'properties': OrderedDict(
|
||||
'properties': dict(
|
||||
[('field1', {'example': 'Example 1'}), ('field2', {'example': 'Example 2'})]
|
||||
),
|
||||
},
|
||||
|
|
@ -100,7 +133,7 @@ def test_schema_to_sample_configuration_generates_config_sequence_of_maps_with_e
|
|||
|
||||
config = module.schema_to_sample_configuration(schema)
|
||||
|
||||
assert config == [OrderedDict([('field1', 'Example 1'), ('field2', 'Example 2')])]
|
||||
assert config == [dict([('field1', 'Example 1'), ('field2', 'Example 2')])]
|
||||
|
||||
|
||||
def test_schema_to_sample_configuration_generates_config_sequence_of_maps_with_multiple_types():
|
||||
|
|
@ -108,7 +141,7 @@ def test_schema_to_sample_configuration_generates_config_sequence_of_maps_with_m
|
|||
'type': 'array',
|
||||
'items': {
|
||||
'type': ['object', 'null'],
|
||||
'properties': OrderedDict(
|
||||
'properties': dict(
|
||||
[('field1', {'example': 'Example 1'}), ('field2', {'example': 'Example 2'})]
|
||||
),
|
||||
},
|
||||
|
|
@ -120,7 +153,7 @@ def test_schema_to_sample_configuration_generates_config_sequence_of_maps_with_m
|
|||
|
||||
config = module.schema_to_sample_configuration(schema)
|
||||
|
||||
assert config == [OrderedDict([('field1', 'Example 1'), ('field2', 'Example 2')])]
|
||||
assert config == [dict([('field1', 'Example 1'), ('field2', 'Example 2')])]
|
||||
|
||||
|
||||
def test_schema_to_sample_configuration_with_unsupported_schema_raises():
|
||||
|
|
@ -133,7 +166,6 @@ def test_schema_to_sample_configuration_with_unsupported_schema_raises():
|
|||
def test_merge_source_configuration_into_destination_inserts_map_fields():
|
||||
destination_config = {'foo': 'dest1', 'bar': 'dest2'}
|
||||
source_config = {'foo': 'source1', 'baz': 'source2'}
|
||||
flexmock(module).should_receive('remove_commented_out_sentinel')
|
||||
flexmock(module).should_receive('ruamel.yaml.comments.CommentedSeq').replace_with(list)
|
||||
|
||||
module.merge_source_configuration_into_destination(destination_config, source_config)
|
||||
|
|
@ -144,7 +176,6 @@ def test_merge_source_configuration_into_destination_inserts_map_fields():
|
|||
def test_merge_source_configuration_into_destination_inserts_nested_map_fields():
|
||||
destination_config = {'foo': {'first': 'dest1', 'second': 'dest2'}, 'bar': 'dest3'}
|
||||
source_config = {'foo': {'first': 'source1'}}
|
||||
flexmock(module).should_receive('remove_commented_out_sentinel')
|
||||
flexmock(module).should_receive('ruamel.yaml.comments.CommentedSeq').replace_with(list)
|
||||
|
||||
module.merge_source_configuration_into_destination(destination_config, source_config)
|
||||
|
|
@ -155,7 +186,6 @@ def test_merge_source_configuration_into_destination_inserts_nested_map_fields()
|
|||
def test_merge_source_configuration_into_destination_inserts_sequence_fields():
|
||||
destination_config = {'foo': ['dest1', 'dest2'], 'bar': ['dest3'], 'baz': ['dest4']}
|
||||
source_config = {'foo': ['source1'], 'bar': ['source2', 'source3']}
|
||||
flexmock(module).should_receive('remove_commented_out_sentinel')
|
||||
flexmock(module).should_receive('ruamel.yaml.comments.CommentedSeq').replace_with(list)
|
||||
|
||||
module.merge_source_configuration_into_destination(destination_config, source_config)
|
||||
|
|
@ -170,7 +200,6 @@ def test_merge_source_configuration_into_destination_inserts_sequence_fields():
|
|||
def test_merge_source_configuration_into_destination_inserts_sequence_of_maps():
|
||||
destination_config = {'foo': [{'first': 'dest1', 'second': 'dest2'}], 'bar': 'dest3'}
|
||||
source_config = {'foo': [{'first': 'source1'}, {'other': 'source2'}]}
|
||||
flexmock(module).should_receive('remove_commented_out_sentinel')
|
||||
flexmock(module).should_receive('ruamel.yaml.comments.CommentedSeq').replace_with(list)
|
||||
|
||||
module.merge_source_configuration_into_destination(destination_config, source_config)
|
||||
|
|
|
|||
|
|
@ -123,6 +123,114 @@ def test_normalize_sections_with_only_scalar_raises():
|
|||
module.normalize_sections('test.yaml', config)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'config,expected_config,produces_logs',
|
||||
(
|
||||
(
|
||||
{'before_actions': ['foo', 'bar'], 'after_actions': ['baz']},
|
||||
{
|
||||
'commands': [
|
||||
{'before': 'repository', 'run': ['foo', 'bar']},
|
||||
{'after': 'repository', 'run': ['baz']},
|
||||
]
|
||||
},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'before_backup': ['foo', 'bar'], 'after_backup': ['baz']},
|
||||
{
|
||||
'commands': [
|
||||
{'before': 'action', 'when': ['create'], 'run': ['foo', 'bar']},
|
||||
{'after': 'action', 'when': ['create'], 'run': ['baz']},
|
||||
]
|
||||
},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'before_prune': ['foo', 'bar'], 'after_prune': ['baz']},
|
||||
{
|
||||
'commands': [
|
||||
{'before': 'action', 'when': ['prune'], 'run': ['foo', 'bar']},
|
||||
{'after': 'action', 'when': ['prune'], 'run': ['baz']},
|
||||
]
|
||||
},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'before_compact': ['foo', 'bar'], 'after_compact': ['baz']},
|
||||
{
|
||||
'commands': [
|
||||
{'before': 'action', 'when': ['compact'], 'run': ['foo', 'bar']},
|
||||
{'after': 'action', 'when': ['compact'], 'run': ['baz']},
|
||||
]
|
||||
},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'before_check': ['foo', 'bar'], 'after_check': ['baz']},
|
||||
{
|
||||
'commands': [
|
||||
{'before': 'action', 'when': ['check'], 'run': ['foo', 'bar']},
|
||||
{'after': 'action', 'when': ['check'], 'run': ['baz']},
|
||||
]
|
||||
},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'before_extract': ['foo', 'bar'], 'after_extract': ['baz']},
|
||||
{
|
||||
'commands': [
|
||||
{'before': 'action', 'when': ['extract'], 'run': ['foo', 'bar']},
|
||||
{'after': 'action', 'when': ['extract'], 'run': ['baz']},
|
||||
]
|
||||
},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'on_error': ['foo', 'bar']},
|
||||
{
|
||||
'commands': [
|
||||
{
|
||||
'after': 'error',
|
||||
'when': ['create', 'prune', 'compact', 'check'],
|
||||
'run': ['foo', 'bar'],
|
||||
},
|
||||
]
|
||||
},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'before_everything': ['foo', 'bar'], 'after_everything': ['baz']},
|
||||
{
|
||||
'commands': [
|
||||
{'before': 'everything', 'when': ['create'], 'run': ['foo', 'bar']},
|
||||
{'after': 'everything', 'when': ['create'], 'run': ['baz']},
|
||||
]
|
||||
},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'other': 'options', 'unrelated_to': 'commands'},
|
||||
{'other': 'options', 'unrelated_to': 'commands'},
|
||||
False,
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_normalize_commands_moves_individual_command_hooks_to_unified_commands(
|
||||
config, expected_config, produces_logs
|
||||
):
|
||||
flexmock(module).should_receive('make_command_hook_deprecation_log').and_return(flexmock())
|
||||
|
||||
logs = module.normalize_commands('test.yaml', config)
|
||||
|
||||
assert config == expected_config
|
||||
|
||||
if produces_logs:
|
||||
assert logs
|
||||
else:
|
||||
assert logs == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'config,expected_config,produces_logs',
|
||||
(
|
||||
|
|
@ -262,6 +370,7 @@ def test_normalize_applies_hard_coded_normalization_to_config(
|
|||
config, expected_config, produces_logs
|
||||
):
|
||||
flexmock(module).should_receive('normalize_sections').and_return([])
|
||||
flexmock(module).should_receive('normalize_commands').and_return([])
|
||||
|
||||
logs = module.normalize('test.yaml', config)
|
||||
expected_config.setdefault('bootstrap', {})
|
||||
|
|
@ -276,6 +385,7 @@ def test_normalize_applies_hard_coded_normalization_to_config(
|
|||
|
||||
def test_normalize_config_with_borgmatic_source_directory_warns():
|
||||
flexmock(module).should_receive('normalize_sections').and_return([])
|
||||
flexmock(module).should_receive('normalize_commands').and_return([])
|
||||
|
||||
logs = module.normalize('test.yaml', {'borgmatic_source_directory': '~/.borgmatic'})
|
||||
|
||||
|
|
|
|||
|
|
@ -94,13 +94,40 @@ def test_normalize_repository_path_passes_through_remote_repository():
|
|||
module.normalize_repository_path(repository) == repository
|
||||
|
||||
|
||||
def test_normalize_repository_path_passes_through_remote_repository_with_base_dir():
|
||||
repository = 'example.org:test.borg'
|
||||
|
||||
flexmock(module.os.path).should_receive('abspath').never()
|
||||
module.normalize_repository_path(repository, '/working') == repository
|
||||
|
||||
|
||||
def test_normalize_repository_path_passes_through_file_repository():
|
||||
repository = 'file:///foo/bar/test.borg'
|
||||
flexmock(module.os.path).should_receive('abspath').and_return('/foo/bar/test.borg')
|
||||
flexmock(module.os.path).should_receive('abspath').with_args('/foo/bar/test.borg').and_return(
|
||||
'/foo/bar/test.borg'
|
||||
)
|
||||
|
||||
module.normalize_repository_path(repository) == '/foo/bar/test.borg'
|
||||
|
||||
|
||||
def test_normalize_repository_path_passes_through_absolute_file_repository_with_base_dir():
|
||||
repository = 'file:///foo/bar/test.borg'
|
||||
flexmock(module.os.path).should_receive('abspath').with_args('/foo/bar/test.borg').and_return(
|
||||
'/foo/bar/test.borg'
|
||||
)
|
||||
|
||||
module.normalize_repository_path(repository, '/working') == '/foo/bar/test.borg'
|
||||
|
||||
|
||||
def test_normalize_repository_path_resolves_relative_file_repository_with_base_dir():
|
||||
repository = 'file://foo/bar/test.borg'
|
||||
flexmock(module.os.path).should_receive('abspath').with_args(
|
||||
'/working/foo/bar/test.borg'
|
||||
).and_return('/working/foo/bar/test.borg')
|
||||
|
||||
module.normalize_repository_path(repository, '/working') == '/working/foo/bar/test.borg'
|
||||
|
||||
|
||||
def test_normalize_repository_path_passes_through_absolute_repository():
|
||||
repository = '/foo/bar/test.borg'
|
||||
flexmock(module.os.path).should_receive('abspath').and_return(repository)
|
||||
|
|
@ -108,14 +135,32 @@ def test_normalize_repository_path_passes_through_absolute_repository():
|
|||
module.normalize_repository_path(repository) == repository
|
||||
|
||||
|
||||
def test_normalize_repository_path_passes_through_absolute_repository_with_base_dir():
|
||||
repository = '/foo/bar/test.borg'
|
||||
flexmock(module.os.path).should_receive('abspath').and_return(repository)
|
||||
|
||||
module.normalize_repository_path(repository, '/working') == repository
|
||||
|
||||
|
||||
def test_normalize_repository_path_resolves_relative_repository():
|
||||
repository = 'test.borg'
|
||||
absolute = '/foo/bar/test.borg'
|
||||
flexmock(module.os.path).should_receive('abspath').and_return(absolute)
|
||||
flexmock(module.os.path).should_receive('abspath').with_args(repository).and_return(absolute)
|
||||
|
||||
module.normalize_repository_path(repository) == absolute
|
||||
|
||||
|
||||
def test_normalize_repository_path_resolves_relative_repository_with_base_dir():
|
||||
repository = 'test.borg'
|
||||
base = '/working'
|
||||
absolute = '/working/test.borg'
|
||||
flexmock(module.os.path).should_receive('abspath').with_args('/working/test.borg').and_return(
|
||||
absolute
|
||||
)
|
||||
|
||||
module.normalize_repository_path(repository, base) == absolute
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'first,second,expected_result',
|
||||
(
|
||||
|
|
@ -202,6 +247,15 @@ def test_guard_configuration_contains_repository_does_not_raise_when_repository_
|
|||
)
|
||||
|
||||
|
||||
def test_guard_configuration_contains_repository_does_not_raise_when_repository_is_none():
|
||||
flexmock(module).should_receive('repositories_match').never()
|
||||
|
||||
module.guard_configuration_contains_repository(
|
||||
repository=None,
|
||||
configurations={'config.yaml': {'repositories': [{'path': 'foo/bar', 'label': 'repo'}]}},
|
||||
)
|
||||
|
||||
|
||||
def test_guard_configuration_contains_repository_errors_when_repository_does_not_match():
|
||||
flexmock(module).should_receive('repositories_match').and_return(False)
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ def test_load_credential_reads_named_credential_from_file():
|
|||
credential_stream = io.StringIO('password')
|
||||
credential_stream.name = '/credentials/mycredential'
|
||||
builtins = flexmock(sys.modules['builtins'])
|
||||
flexmock(module.os.path).should_receive('expanduser').with_args(
|
||||
'/credentials/mycredential'
|
||||
).and_return('/credentials/mycredential')
|
||||
builtins.should_receive('open').with_args('/credentials/mycredential').and_return(
|
||||
credential_stream
|
||||
)
|
||||
|
|
@ -42,6 +45,9 @@ def test_load_credential_reads_named_credential_from_file_using_working_director
|
|||
credential_stream = io.StringIO('password')
|
||||
credential_stream.name = '/working/credentials/mycredential'
|
||||
builtins = flexmock(sys.modules['builtins'])
|
||||
flexmock(module.os.path).should_receive('expanduser').with_args(
|
||||
'credentials/mycredential'
|
||||
).and_return('credentials/mycredential')
|
||||
builtins.should_receive('open').with_args('/working/credentials/mycredential').and_return(
|
||||
credential_stream
|
||||
)
|
||||
|
|
@ -58,6 +64,9 @@ def test_load_credential_reads_named_credential_from_file_using_working_director
|
|||
|
||||
def test_load_credential_with_file_not_found_error_raises():
|
||||
builtins = flexmock(sys.modules['builtins'])
|
||||
flexmock(module.os.path).should_receive('expanduser').with_args(
|
||||
'/credentials/mycredential'
|
||||
).and_return('/credentials/mycredential')
|
||||
builtins.should_receive('open').with_args('/credentials/mycredential').and_raise(
|
||||
FileNotFoundError
|
||||
)
|
||||
|
|
@ -66,3 +75,22 @@ def test_load_credential_with_file_not_found_error_raises():
|
|||
module.load_credential(
|
||||
hook_config={}, config={}, credential_parameters=('/credentials/mycredential',)
|
||||
)
|
||||
|
||||
|
||||
def test_load_credential_reads_named_credential_from_expanded_directory():
|
||||
credential_stream = io.StringIO('password')
|
||||
credential_stream.name = '/root/credentials/mycredential'
|
||||
builtins = flexmock(sys.modules['builtins'])
|
||||
flexmock(module.os.path).should_receive('expanduser').with_args(
|
||||
'~/credentials/mycredential'
|
||||
).and_return('/root/credentials/mycredential')
|
||||
builtins.should_receive('open').with_args('/root/credentials/mycredential').and_return(
|
||||
credential_stream
|
||||
)
|
||||
|
||||
assert (
|
||||
module.load_credential(
|
||||
hook_config={}, config={}, credential_parameters=('~/credentials/mycredential',)
|
||||
)
|
||||
== 'password'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ def test_load_credential_with_invalid_credential_parameters_raises(credential_pa
|
|||
|
||||
|
||||
def test_load_credential_with_missing_database_raises():
|
||||
flexmock(module.os.path).should_receive('expanduser').with_args('database.kdbx').and_return(
|
||||
'database.kdbx'
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command_and_capture_output').never()
|
||||
|
||||
|
|
@ -25,6 +28,9 @@ def test_load_credential_with_missing_database_raises():
|
|||
|
||||
|
||||
def test_load_credential_with_present_database_fetches_password_from_keepassxc():
|
||||
flexmock(module.os.path).should_receive('expanduser').with_args('database.kdbx').and_return(
|
||||
'database.kdbx'
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||
flexmock(module.borgmatic.execute).should_receive(
|
||||
'execute_command_and_capture_output'
|
||||
|
|
@ -51,6 +57,9 @@ def test_load_credential_with_present_database_fetches_password_from_keepassxc()
|
|||
|
||||
|
||||
def test_load_credential_with_custom_keepassxc_cli_command_calls_it():
|
||||
flexmock(module.os.path).should_receive('expanduser').with_args('database.kdbx').and_return(
|
||||
'database.kdbx'
|
||||
)
|
||||
config = {'keepassxc': {'keepassxc_cli_command': '/usr/local/bin/keepassxc-cli --some-option'}}
|
||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||
flexmock(module.borgmatic.execute).should_receive(
|
||||
|
|
@ -78,3 +87,32 @@ def test_load_credential_with_custom_keepassxc_cli_command_calls_it():
|
|||
)
|
||||
== 'password'
|
||||
)
|
||||
|
||||
|
||||
def test_load_credential_with_expanded_directory_with_present_database_fetches_password_from_keepassxc():
|
||||
flexmock(module.os.path).should_receive('expanduser').with_args('~/database.kdbx').and_return(
|
||||
'/root/database.kdbx'
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||
flexmock(module.borgmatic.execute).should_receive(
|
||||
'execute_command_and_capture_output'
|
||||
).with_args(
|
||||
(
|
||||
'keepassxc-cli',
|
||||
'show',
|
||||
'--show-protected',
|
||||
'--attributes',
|
||||
'Password',
|
||||
'/root/database.kdbx',
|
||||
'mypassword',
|
||||
)
|
||||
).and_return(
|
||||
'password'
|
||||
).once()
|
||||
|
||||
assert (
|
||||
module.load_credential(
|
||||
hook_config={}, config={}, credential_parameters=('~/database.kdbx', 'mypassword')
|
||||
)
|
||||
== 'password'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -681,3 +681,135 @@ def test_restore_data_source_dump_without_extract_process_restores_from_disk():
|
|||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_uses_custom_mongodump_command():
|
||||
flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
|
||||
flexmock()
|
||||
)
|
||||
databases = [{'name': 'foo', 'mongodump_command': 'custom_mongodump'}]
|
||||
process = flexmock()
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'databases/localhost/foo'
|
||||
)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
(
|
||||
'custom_mongodump',
|
||||
'--db',
|
||||
'foo',
|
||||
'--archive',
|
||||
'>',
|
||||
'databases/localhost/foo',
|
||||
),
|
||||
shell=True,
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
patterns=[],
|
||||
dry_run=False,
|
||||
) == [process]
|
||||
|
||||
|
||||
def test_build_dump_command_prevents_shell_injection():
|
||||
database = {
|
||||
'name': 'testdb; rm -rf /', # Malicious input
|
||||
'hostname': 'localhost',
|
||||
'port': 27017,
|
||||
'username': 'user',
|
||||
'password': 'password',
|
||||
'mongodump_command': 'mongodump',
|
||||
'options': '--gzip',
|
||||
}
|
||||
config = {}
|
||||
dump_filename = '/path/to/dump'
|
||||
dump_format = 'archive'
|
||||
|
||||
command = module.build_dump_command(database, config, dump_filename, dump_format)
|
||||
|
||||
# Ensure the malicious input is properly escaped and does not execute
|
||||
assert 'testdb; rm -rf /' not in command
|
||||
assert any(
|
||||
'testdb' in part for part in command
|
||||
) # Check if 'testdb' is in any part of the tuple
|
||||
|
||||
|
||||
def test_restore_data_source_dump_uses_custom_mongorestore_command():
|
||||
hook_config = [
|
||||
{
|
||||
'name': 'foo',
|
||||
'mongorestore_command': 'custom_mongorestore',
|
||||
'schemas': None,
|
||||
'restore_options': '--gzip',
|
||||
}
|
||||
]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('make_dump_path')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename')
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
).replace_with(lambda value, config: value)
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
[
|
||||
'custom_mongorestore', # Should use custom command instead of default
|
||||
'--archive',
|
||||
'--drop',
|
||||
'--gzip', # Should include restore options
|
||||
],
|
||||
processes=[extract_process],
|
||||
output_log_level=logging.DEBUG,
|
||||
input_file=extract_process.stdout,
|
||||
).once()
|
||||
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
'hostname': None,
|
||||
'port': None,
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
def test_build_restore_command_prevents_shell_injection():
|
||||
database = {
|
||||
'name': 'testdb; rm -rf /', # Malicious input
|
||||
'restore_hostname': 'localhost',
|
||||
'restore_port': 27017,
|
||||
'restore_username': 'user',
|
||||
'restore_password': 'password',
|
||||
'mongorestore_command': 'mongorestore',
|
||||
'restore_options': '--gzip',
|
||||
}
|
||||
config = {}
|
||||
dump_filename = '/path/to/dump'
|
||||
connection_params = {
|
||||
'hostname': None,
|
||||
'port': None,
|
||||
'username': None,
|
||||
'password': None,
|
||||
}
|
||||
extract_process = None
|
||||
|
||||
command = module.build_restore_command(
|
||||
extract_process, database, config, dump_filename, connection_params
|
||||
)
|
||||
|
||||
# print(command)
|
||||
# Ensure the malicious input is properly escaped and does not execute
|
||||
assert 'rm -rf /' not in command
|
||||
assert ';' not in command
|
||||
|
|
|
|||
|
|
@ -107,6 +107,48 @@ def test_dump_data_sources_with_path_injection_attack_gets_escaped():
|
|||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_runs_non_default_sqlite_with_path_injection_attack_gets_escaped():
|
||||
databases = [
|
||||
{
|
||||
'path': '/path/to/database1; naughty-command',
|
||||
'name': 'database1',
|
||||
'sqlite_command': 'custom_sqlite *',
|
||||
},
|
||||
]
|
||||
processes = [flexmock()]
|
||||
|
||||
flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'/run/borgmatic/database'
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
(
|
||||
'custom_sqlite', # custom sqlite command
|
||||
"'*'", # Should get shell escaped to prevent injection attacks.
|
||||
"'/path/to/database1; naughty-command'",
|
||||
'.dump',
|
||||
'>',
|
||||
'/run/borgmatic/database',
|
||||
),
|
||||
shell=True,
|
||||
run_to_completion=False,
|
||||
).and_return(processes[0])
|
||||
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
patterns=[],
|
||||
dry_run=False,
|
||||
)
|
||||
== processes
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_with_non_existent_path_warns_and_dumps_database():
|
||||
databases = [
|
||||
{'path': '/path/to/database1', 'name': 'database1'},
|
||||
|
|
@ -216,6 +258,41 @@ def test_restore_data_source_dump_restores_database():
|
|||
)
|
||||
|
||||
|
||||
def test_restore_data_source_dump_runs_non_default_sqlite_restores_database():
|
||||
hook_config = [
|
||||
{
|
||||
'path': '/path/to/database',
|
||||
'name': 'database',
|
||||
'sqlite_restore_command': 'custom_sqlite *',
|
||||
},
|
||||
{'name': 'other'},
|
||||
]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
(
|
||||
'custom_sqlite',
|
||||
"'*'", # Should get shell escaped to prevent injection attacks.
|
||||
'/path/to/database',
|
||||
),
|
||||
processes=[extract_process],
|
||||
output_log_level=logging.DEBUG,
|
||||
input_file=extract_process.stdout,
|
||||
).once()
|
||||
|
||||
flexmock(module.os).should_receive('remove').once()
|
||||
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={'restore_path': None},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
def test_restore_data_source_dump_with_connection_params_uses_connection_params_for_restore():
|
||||
hook_config = [
|
||||
{'path': '/path/to/database', 'name': 'database', 'restore_path': 'config/path/to/database'}
|
||||
|
|
@ -245,6 +322,38 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
|
|||
)
|
||||
|
||||
|
||||
def test_restore_data_source_dump_runs_non_default_sqlite_with_connection_params_uses_connection_params_for_restore():
|
||||
hook_config = [
|
||||
{'path': '/path/to/database', 'name': 'database', 'restore_path': 'config/path/to/database'}
|
||||
]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
(
|
||||
'custom_sqlite',
|
||||
'cli/path/to/database',
|
||||
),
|
||||
processes=[extract_process],
|
||||
output_log_level=logging.DEBUG,
|
||||
input_file=extract_process.stdout,
|
||||
).once()
|
||||
|
||||
flexmock(module.os).should_receive('remove').once()
|
||||
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
data_source={
|
||||
'name': 'database',
|
||||
'sqlite_restore_command': 'custom_sqlite',
|
||||
},
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={'restore_path': 'cli/path/to/database'},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
def test_restore_data_source_dump_without_connection_params_uses_restore_params_in_config_for_restore():
|
||||
hook_config = [
|
||||
{'path': '/path/to/database', 'name': 'database', 'restore_path': 'config/path/to/database'}
|
||||
|
|
@ -274,6 +383,40 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
|
|||
)
|
||||
|
||||
|
||||
def test_restore_data_source_dump_runs_non_default_sqlite_without_connection_params_uses_restore_params_in_config_for_restore():
|
||||
hook_config = [
|
||||
{
|
||||
'path': '/path/to/database',
|
||||
'name': 'database',
|
||||
'sqlite_restore_command': 'custom_sqlite',
|
||||
'restore_path': 'config/path/to/database',
|
||||
}
|
||||
]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
(
|
||||
'custom_sqlite',
|
||||
'config/path/to/database',
|
||||
),
|
||||
processes=[extract_process],
|
||||
output_log_level=logging.DEBUG,
|
||||
input_file=extract_process.stdout,
|
||||
).once()
|
||||
|
||||
flexmock(module.os).should_receive('remove').once()
|
||||
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={'restore_path': None},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
def test_restore_data_source_dump_does_not_restore_database_if_dry_run():
|
||||
hook_config = [{'path': '/path/to/database', 'name': 'database'}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import logging
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.hooks import command as module
|
||||
|
|
@ -48,43 +49,161 @@ def test_make_environment_with_pyinstaller_and_LD_LIBRARY_PATH_ORIG_copies_it_in
|
|||
) == {'LD_LIBRARY_PATH_ORIG': '/lib/lib/lib', 'LD_LIBRARY_PATH': '/lib/lib/lib'}
|
||||
|
||||
|
||||
def test_execute_hook_invokes_each_command():
|
||||
@pytest.mark.parametrize(
|
||||
'hooks,filters,expected_hooks',
|
||||
(
|
||||
(
|
||||
(
|
||||
{
|
||||
'before': 'action',
|
||||
'run': ['foo'],
|
||||
},
|
||||
{
|
||||
'after': 'action',
|
||||
'run': ['bar'],
|
||||
},
|
||||
{
|
||||
'before': 'repository',
|
||||
'run': ['baz'],
|
||||
},
|
||||
),
|
||||
{},
|
||||
(
|
||||
{
|
||||
'before': 'action',
|
||||
'run': ['foo'],
|
||||
},
|
||||
{
|
||||
'after': 'action',
|
||||
'run': ['bar'],
|
||||
},
|
||||
{
|
||||
'before': 'repository',
|
||||
'run': ['baz'],
|
||||
},
|
||||
),
|
||||
),
|
||||
(
|
||||
(
|
||||
{
|
||||
'before': 'action',
|
||||
'run': ['foo'],
|
||||
},
|
||||
{
|
||||
'after': 'action',
|
||||
'run': ['bar'],
|
||||
},
|
||||
{
|
||||
'before': 'repository',
|
||||
'run': ['baz'],
|
||||
},
|
||||
),
|
||||
{
|
||||
'before': 'action',
|
||||
},
|
||||
(
|
||||
{
|
||||
'before': 'action',
|
||||
'run': ['foo'],
|
||||
},
|
||||
),
|
||||
),
|
||||
(
|
||||
(
|
||||
{
|
||||
'after': 'action',
|
||||
'run': ['foo'],
|
||||
},
|
||||
{
|
||||
'before': 'action',
|
||||
'run': ['bar'],
|
||||
},
|
||||
{
|
||||
'after': 'repository',
|
||||
'run': ['baz'],
|
||||
},
|
||||
),
|
||||
{
|
||||
'after': 'action',
|
||||
},
|
||||
(
|
||||
{
|
||||
'after': 'action',
|
||||
'run': ['foo'],
|
||||
},
|
||||
),
|
||||
),
|
||||
(
|
||||
(
|
||||
{
|
||||
'before': 'action',
|
||||
'run': ['foo'],
|
||||
},
|
||||
{
|
||||
'before': 'action',
|
||||
'run': ['bar'],
|
||||
},
|
||||
{
|
||||
'before': 'action',
|
||||
'run': ['baz'],
|
||||
},
|
||||
),
|
||||
{
|
||||
'before': 'action',
|
||||
'action_names': ['create', 'compact', 'extract'],
|
||||
},
|
||||
(
|
||||
{
|
||||
'before': 'action',
|
||||
'run': ['foo'],
|
||||
},
|
||||
{
|
||||
'before': 'action',
|
||||
'run': ['bar'],
|
||||
},
|
||||
{
|
||||
'before': 'action',
|
||||
'run': ['baz'],
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_filter_hooks(hooks, filters, expected_hooks):
|
||||
assert module.filter_hooks(hooks, **filters) == expected_hooks
|
||||
|
||||
|
||||
LOGGING_ANSWER = flexmock()
|
||||
|
||||
|
||||
def test_execute_hooks_invokes_each_hook_and_command():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module).should_receive('interpolate_context').replace_with(
|
||||
lambda hook_description, command, context: command
|
||||
)
|
||||
flexmock(module).should_receive('make_environment').and_return({})
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
[':'],
|
||||
output_log_level=logging.WARNING,
|
||||
shell=True,
|
||||
environment={},
|
||||
).once()
|
||||
|
||||
module.execute_hook([':'], None, 'config.yaml', 'pre-backup', dry_run=False)
|
||||
for command in ('foo', 'bar', 'baz'):
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
[command],
|
||||
output_log_level=LOGGING_ANSWER,
|
||||
shell=True,
|
||||
environment={},
|
||||
working_directory=None,
|
||||
).once()
|
||||
|
||||
|
||||
def test_execute_hook_with_multiple_commands_invokes_each_command():
|
||||
flexmock(module).should_receive('interpolate_context').replace_with(
|
||||
lambda hook_description, command, context: command
|
||||
module.execute_hooks(
|
||||
[{'before': 'create', 'run': ['foo']}, {'before': 'create', 'run': ['bar', 'baz']}],
|
||||
umask=None,
|
||||
working_directory=None,
|
||||
dry_run=False,
|
||||
)
|
||||
flexmock(module).should_receive('make_environment').and_return({})
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
[':'],
|
||||
output_log_level=logging.WARNING,
|
||||
shell=True,
|
||||
environment={},
|
||||
).once()
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
['true'],
|
||||
output_log_level=logging.WARNING,
|
||||
shell=True,
|
||||
environment={},
|
||||
).once()
|
||||
|
||||
module.execute_hook([':', 'true'], None, 'config.yaml', 'pre-backup', dry_run=False)
|
||||
|
||||
|
||||
def test_execute_hook_with_umask_sets_that_umask():
|
||||
def test_execute_hooks_with_umask_sets_that_umask():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module).should_receive('interpolate_context').replace_with(
|
||||
lambda hook_description, command, context: command
|
||||
)
|
||||
|
|
@ -92,42 +211,292 @@ def test_execute_hook_with_umask_sets_that_umask():
|
|||
flexmock(module.os).should_receive('umask').with_args(0o22).once()
|
||||
flexmock(module).should_receive('make_environment').and_return({})
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
[':'],
|
||||
output_log_level=logging.WARNING,
|
||||
['foo'],
|
||||
output_log_level=logging.ANSWER,
|
||||
shell=True,
|
||||
environment={},
|
||||
working_directory=None,
|
||||
)
|
||||
|
||||
module.execute_hook([':'], 77, 'config.yaml', 'pre-backup', dry_run=False)
|
||||
module.execute_hooks(
|
||||
[{'before': 'create', 'run': ['foo']}], umask=77, working_directory=None, dry_run=False
|
||||
)
|
||||
|
||||
|
||||
def test_execute_hook_with_dry_run_skips_commands():
|
||||
def test_execute_hooks_with_working_directory_executes_command_with_it():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module).should_receive('interpolate_context').replace_with(
|
||||
lambda hook_description, command, context: command
|
||||
)
|
||||
flexmock(module).should_receive('make_environment').and_return({})
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
['foo'],
|
||||
output_log_level=logging.ANSWER,
|
||||
shell=True,
|
||||
environment={},
|
||||
working_directory='/working',
|
||||
)
|
||||
|
||||
module.execute_hooks(
|
||||
[{'before': 'create', 'run': ['foo']}],
|
||||
umask=None,
|
||||
working_directory='/working',
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_execute_hooks_with_dry_run_skips_commands():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module).should_receive('interpolate_context').replace_with(
|
||||
lambda hook_description, command, context: command
|
||||
)
|
||||
flexmock(module).should_receive('make_environment').and_return({})
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').never()
|
||||
|
||||
module.execute_hook([':', 'true'], None, 'config.yaml', 'pre-backup', dry_run=True)
|
||||
module.execute_hooks(
|
||||
[{'before': 'create', 'run': ['foo']}], umask=None, working_directory=None, dry_run=True
|
||||
)
|
||||
|
||||
|
||||
def test_execute_hook_with_empty_commands_does_not_raise():
|
||||
module.execute_hook([], None, 'config.yaml', 'post-backup', dry_run=False)
|
||||
def test_execute_hooks_with_empty_commands_does_not_raise():
|
||||
module.execute_hooks([], umask=None, working_directory=None, dry_run=True)
|
||||
|
||||
|
||||
def test_execute_hook_on_error_logs_as_error():
|
||||
def test_execute_hooks_with_error_logs_as_error():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module).should_receive('interpolate_context').replace_with(
|
||||
lambda hook_description, command, context: command
|
||||
)
|
||||
flexmock(module).should_receive('make_environment').and_return({})
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
[':'],
|
||||
['foo'],
|
||||
output_log_level=logging.ERROR,
|
||||
shell=True,
|
||||
environment={},
|
||||
working_directory=None,
|
||||
).once()
|
||||
|
||||
module.execute_hook([':'], None, 'config.yaml', 'on-error', dry_run=False)
|
||||
module.execute_hooks(
|
||||
[{'after': 'error', 'run': ['foo']}], umask=None, working_directory=None, dry_run=False
|
||||
)
|
||||
|
||||
|
||||
def test_execute_hooks_with_before_or_after_raises():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module).should_receive('interpolate_context').never()
|
||||
flexmock(module).should_receive('make_environment').never()
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').never()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.execute_hooks(
|
||||
[
|
||||
{'erstwhile': 'create', 'run': ['foo']},
|
||||
{'erstwhile': 'create', 'run': ['bar', 'baz']},
|
||||
],
|
||||
umask=None,
|
||||
working_directory=None,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_execute_hooks_without_commands_to_run_does_not_raise():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module).should_receive('interpolate_context').replace_with(
|
||||
lambda hook_description, command, context: command
|
||||
)
|
||||
flexmock(module).should_receive('make_environment').and_return({})
|
||||
|
||||
for command in ('foo', 'bar'):
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
[command],
|
||||
output_log_level=LOGGING_ANSWER,
|
||||
shell=True,
|
||||
environment={},
|
||||
working_directory=None,
|
||||
).once()
|
||||
|
||||
module.execute_hooks(
|
||||
[{'before': 'create', 'run': []}, {'before': 'create', 'run': ['foo', 'bar']}],
|
||||
umask=None,
|
||||
working_directory=None,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_before_after_hooks_calls_command_hooks():
|
||||
commands = [
|
||||
{'before': 'repository', 'run': ['foo', 'bar']},
|
||||
{'after': 'repository', 'run': ['baz']},
|
||||
]
|
||||
flexmock(module).should_receive('filter_hooks').with_args(
|
||||
commands,
|
||||
before='action',
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
).and_return(flexmock()).once()
|
||||
flexmock(module).should_receive('filter_hooks').with_args(
|
||||
commands,
|
||||
after='action',
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
).and_return(flexmock()).once()
|
||||
flexmock(module).should_receive('execute_hooks').twice()
|
||||
|
||||
with module.Before_after_hooks(
|
||||
command_hooks=commands,
|
||||
before_after='action',
|
||||
umask=1234,
|
||||
working_directory='/working',
|
||||
dry_run=False,
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
context1='stuff',
|
||||
context2='such',
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
def test_before_after_hooks_with_before_error_runs_after_hook_and_raises():
|
||||
commands = [
|
||||
{'before': 'repository', 'run': ['foo', 'bar']},
|
||||
{'after': 'repository', 'run': ['baz']},
|
||||
]
|
||||
flexmock(module).should_receive('filter_hooks').with_args(
|
||||
commands,
|
||||
before='action',
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
).and_return(flexmock()).once()
|
||||
flexmock(module).should_receive('filter_hooks').with_args(
|
||||
commands,
|
||||
after='action',
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
).and_return(flexmock()).once()
|
||||
flexmock(module).should_receive('execute_hooks').and_raise(OSError).and_return(None)
|
||||
flexmock(module).should_receive('considered_soft_failure').and_return(False)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with module.Before_after_hooks(
|
||||
command_hooks=commands,
|
||||
before_after='action',
|
||||
umask=1234,
|
||||
working_directory='/working',
|
||||
dry_run=False,
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
context1='stuff',
|
||||
context2='such',
|
||||
):
|
||||
assert False # This should never get called.
|
||||
|
||||
|
||||
def test_before_after_hooks_with_before_soft_failure_does_not_raise():
|
||||
commands = [
|
||||
{'before': 'repository', 'run': ['foo', 'bar']},
|
||||
{'after': 'repository', 'run': ['baz']},
|
||||
]
|
||||
flexmock(module).should_receive('filter_hooks').with_args(
|
||||
commands,
|
||||
before='action',
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
).and_return(flexmock()).once()
|
||||
flexmock(module).should_receive('filter_hooks').with_args(
|
||||
commands,
|
||||
after='action',
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
).and_return(flexmock()).once()
|
||||
flexmock(module).should_receive('execute_hooks').and_raise(OSError)
|
||||
flexmock(module).should_receive('considered_soft_failure').and_return(True)
|
||||
|
||||
with module.Before_after_hooks(
|
||||
command_hooks=commands,
|
||||
before_after='action',
|
||||
umask=1234,
|
||||
working_directory='/working',
|
||||
dry_run=False,
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
context1='stuff',
|
||||
context2='such',
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
def test_before_after_hooks_with_after_error_raises():
|
||||
commands = [
|
||||
{'before': 'repository', 'run': ['foo', 'bar']},
|
||||
{'after': 'repository', 'run': ['baz']},
|
||||
]
|
||||
flexmock(module).should_receive('filter_hooks').with_args(
|
||||
commands,
|
||||
before='action',
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
).and_return(flexmock()).once()
|
||||
flexmock(module).should_receive('filter_hooks').with_args(
|
||||
commands,
|
||||
after='action',
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
).and_return(flexmock()).once()
|
||||
flexmock(module).should_receive('execute_hooks').and_return(None).and_raise(OSError)
|
||||
flexmock(module).should_receive('considered_soft_failure').and_return(False)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with module.Before_after_hooks(
|
||||
command_hooks=commands,
|
||||
before_after='action',
|
||||
umask=1234,
|
||||
working_directory='/working',
|
||||
dry_run=False,
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
context1='stuff',
|
||||
context2='such',
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
def test_before_after_hooks_with_after_soft_failure_does_not_raise():
|
||||
commands = [
|
||||
{'before': 'repository', 'run': ['foo', 'bar']},
|
||||
{'after': 'repository', 'run': ['baz']},
|
||||
]
|
||||
flexmock(module).should_receive('filter_hooks').with_args(
|
||||
commands,
|
||||
before='action',
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
).and_return(flexmock()).once()
|
||||
flexmock(module).should_receive('filter_hooks').with_args(
|
||||
commands,
|
||||
after='action',
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
).and_return(flexmock()).once()
|
||||
flexmock(module).should_receive('execute_hooks').and_return(None).and_raise(OSError)
|
||||
flexmock(module).should_receive('considered_soft_failure').and_return(True)
|
||||
|
||||
with module.Before_after_hooks(
|
||||
command_hooks=commands,
|
||||
before_after='action',
|
||||
umask=1234,
|
||||
working_directory='/working',
|
||||
dry_run=False,
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
context1='stuff',
|
||||
context2='such',
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
def test_considered_soft_failure_treats_soft_fail_exit_code_as_soft_fail():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue