Compare commits

..
21 Commits
Author SHA1 Message Date
Dan Helfman 50096296da Revamp systemd credential syntax to be more consistent with constants (#966). 2025-02-10 22:01:23 -08:00
Dan Helfman 3bc14ba364 Bump version for release. 2025-02-10 14:21:33 -08:00
Dan Helfman c9c6913547 Add a "!credential" tag for loading systemd credentials into borgmatic configuration (#966).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/993
2025-02-10 22:18:43 +00:00
Dan Helfman 779f51f40a Fix favicon on non-home pages. 2025-02-10 13:24:27 -08:00
Dan Helfman 24b846e9ca Additional test coverage (#966). 2025-02-10 10:05:51 -08:00
Dan Helfman 73fe29b055 Add additional test coverage for credential tag (#966). 2025-02-10 09:52:07 -08:00
Dan Helfman 775385e688 Get unit tests passing again (#966). 2025-02-09 22:44:38 -08:00
Dan Helfman efdbee934a Update documentation to describe delayed !credential tag approach (#966). 2025-02-09 15:27:58 -08:00
Dan Helfman 49719dc309 Load credentials from database hooks (#966). 2025-02-09 11:35:26 -08:00
Dan Helfman b7e3ee8277 Revamped the credentials to load them much closer to where they're used (#966). 2025-02-09 11:12:40 -08:00
Dan Helfman 97fe1a2c50 Flake fixes (#966). 2025-02-08 19:28:03 -08:00
Dan Helfman 66abf38b39 Add end-to-end tests for the systemd credential hook (#966). 2025-02-08 17:50:59 -08:00
Dan Helfman 5baf091853 Add automated tests for the systemd credential hook (#966). 2025-02-08 10:42:11 -08:00
Dan Helfman c5abcc1fdf Add documentation for the "!credential" tag (#966). 2025-02-07 16:04:10 -08:00
Dan Helfman 9a9a8fd1c6 Add a "!credential" tag for loading systemd credentials into borgmatic configuration (#966). 2025-02-07 14:09:26 -08:00
Dan Helfman ab9e8d06ee Add a delayed logging handler that delays anything logged before logging is actually configured. 2025-02-07 09:50:05 -08:00
Dan Helfman 5a2cd1b261 Add support for Python 3.13. 2025-02-06 14:21:36 -08:00
Dan Helfman ffaa99ba15 With the "max_duration" option or the "--max-duration" flag, run the archives and repository checks separately so they don't interfere with one another (#988). 2025-02-06 11:52:16 -08:00
Dan Helfman 5dc0b08f22 Fix the log message code to avoid using Python 3.10+ logging features (#989). 2025-02-04 11:51:39 -08:00
Dan Helfman 23009e22aa When both "encryption_passcommand" and "encryption_passphrase" are configured, prefer "encryption_passphrase" even if it's an empty value (#987). 2025-02-03 23:20:31 -08:00
Dan Helfman 6cfa10fb7e Fix a "list" action error when the "encryption_passcommand" option is set (#987). 2025-02-03 23:11:59 -08:00
54 changed files with 1632 additions and 351 deletions
+16
View File
@@ -1,3 +1,19 @@
1.9.10
* #966: Add a "{credential ...}" syntax for loading systemd credentials into borgmatic
configuration files. See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/provide-your-passwords/
* #987: Fix a "list" action error when the "encryption_passcommand" option is set.
* #987: When both "encryption_passcommand" and "encryption_passphrase" are configured, prefer
"encryption_passphrase" even if it's an empty value.
* #988: With the "max_duration" option or the "--max-duration" flag, run the archives and
repository checks separately so they don't interfere with one another. Previously, borgmatic
refused to run checks in this situation.
* #989: Fix the log message code to avoid using Python 3.10+ logging features. Now borgmatic will
work with Python 3.9 again.
* Capture and delay any log records produced before logging is fully configured, so early log
records don't get lost.
* Add support for Python 3.13.
1.9.9
* #635: Log the repository path or label on every relevant log message, not just some logs.
* #961: When the "encryption_passcommand" option is set, call the command once from borgmatic to
+12 -1
View File
@@ -56,6 +56,8 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
## Integrations
### Data
<a href="https://www.postgresql.org/"><img src="docs/static/postgresql.png" alt="PostgreSQL" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://www.mysql.com/"><img src="docs/static/mysql.png" alt="MySQL" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://mariadb.com/"><img src="docs/static/mariadb.png" alt="MariaDB" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
@@ -65,6 +67,11 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
<a href="https://btrfs.readthedocs.io/"><img src="docs/static/btrfs.png" alt="Btrfs" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://sourceware.org/lvm2/"><img src="docs/static/lvm.png" alt="LVM" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://rclone.org"><img src="docs/static/rclone.png" alt="rclone" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://www.borgbase.com/?utm_source=borgmatic"><img src="docs/static/borgbase.png" alt="BorgBase" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
### Monitoring
<a href="https://healthchecks.io/"><img src="docs/static/healthchecks.png" alt="Healthchecks" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://uptime.kuma.pet/"><img src="docs/static/uptimekuma.png" alt="Uptime Kuma" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://cronitor.io/"><img src="docs/static/cronitor.png" alt="Cronitor" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
@@ -76,7 +83,11 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
<a href="https://github.com/caronc/apprise/wiki"><img src="docs/static/apprise.png" alt="Apprise" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://www.zabbix.com/"><img src="docs/static/zabbix.png" alt="Zabbix" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://sentry.io/"><img src="docs/static/sentry.png" alt="Sentry" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://www.borgbase.com/?utm_source=borgmatic"><img src="docs/static/borgbase.png" alt="BorgBase" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
### Credentials
<a href="https://systemd.io/"><img src="docs/static/systemd.png" alt="Sentry" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
## Getting started
+1 -2
View File
@@ -386,13 +386,12 @@ def collect_spot_check_source_paths(
stream_processes=stream_processes,
)
)
borg_environment = borgmatic.borg.environment.make_environment(config)
working_directory = borgmatic.config.paths.get_working_directory(config)
paths_output = borgmatic.execute.execute_command_and_capture_output(
create_flags + create_positional_arguments,
capture_stderr=True,
extra_environment=borg_environment,
extra_environment=borgmatic.borg.environment.make_environment(config),
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
+1 -2
View File
@@ -34,10 +34,9 @@ def break_lock(
+ flags.make_repository_flags(repository_path, local_borg_version)
)
borg_environment = environment.make_environment(config)
execute_command(
full_command,
extra_environment=borg_environment,
extra_environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
+42 -46
View File
@@ -64,15 +64,11 @@ def make_check_name_flags(checks, archive_filter_flags):
('--repository-only',)
However, if both "repository" and "archives" are in checks, then omit them from the returned
flags because Borg does both checks by default. If "data" is in checks, that implies "archives".
However, if both "repository" and "archives" are in checks, then omit the "only" flags from the
returned flags because Borg does both checks by default. Note that a "data" check only works
along with an "archives" check.
'''
if 'data' in checks:
data_flags = ('--verify-data',)
checks.update({'archives'})
else:
data_flags = ()
data_flags = ('--verify-data',) if 'data' in checks else ()
common_flags = (archive_filter_flags if 'archives' in checks else ()) + data_flags
if {'repository', 'archives'}.issubset(checks):
@@ -142,51 +138,51 @@ def check_archives(
except StopIteration:
repository_check_config = {}
if check_arguments.max_duration and 'archives' in checks:
raise ValueError('The archives check cannot run when the --max-duration flag is used')
if repository_check_config.get('max_duration') and 'archives' in checks:
raise ValueError(
'The archives check cannot run when the repository check has the max_duration option set'
)
max_duration = check_arguments.max_duration or repository_check_config.get('max_duration')
umask = config.get('umask')
borg_environment = environment.make_environment(config)
borg_exit_codes = config.get('borg_exit_codes')
full_command = (
(local_path, 'check')
+ (('--repair',) if check_arguments.repair else ())
+ (('--max-duration', str(max_duration)) if max_duration else ())
+ make_check_name_flags(checks, archive_filter_flags)
+ (('--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 ())
+ verbosity_flags
+ (('--progress',) if check_arguments.progress else ())
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
+ flags.make_repository_flags(repository_path, local_borg_version)
)
working_directory = borgmatic.config.paths.get_working_directory(config)
# The Borg repair option triggers an interactive prompt, which won't work when output is
# captured. And progress messes with the terminal directly.
if check_arguments.repair or check_arguments.progress:
if 'data' in checks:
checks.add('archives')
grouped_checks = (checks,)
# If max_duration is set, then archives and repository checks need to be run separately, as Borg
# doesn't support --max-duration along with an archives checks.
if max_duration and 'archives' in checks and 'repository' in checks:
checks.remove('repository')
grouped_checks = (checks, {'repository'})
for checks_subset in grouped_checks:
full_command = (
(local_path, 'check')
+ (('--repair',) if check_arguments.repair else ())
+ (
('--max-duration', str(max_duration))
if max_duration and 'repository' in checks_subset
else ()
)
+ make_check_name_flags(checks_subset, archive_filter_flags)
+ (('--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 ())
+ verbosity_flags
+ (('--progress',) if check_arguments.progress else ())
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
+ flags.make_repository_flags(repository_path, local_borg_version)
)
execute_command(
full_command,
output_file=DO_NOT_CAPTURE,
extra_environment=borg_environment,
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
else:
execute_command(
full_command,
extra_environment=borg_environment,
# The Borg repair option triggers an interactive prompt, which won't work when output is
# captured. And progress messes with the terminal directly.
output_file=(
DO_NOT_CAPTURE if check_arguments.repair or check_arguments.progress else None
),
extra_environment=environment.make_environment(config),
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
+8 -13
View File
@@ -122,15 +122,14 @@ def collect_special_file_paths(
config,
local_path,
working_directory,
borg_environment,
borgmatic_runtime_directory,
):
'''
Given a dry-run flag, a Borg create command as a tuple, a configuration dict, a local Borg path,
a working directory, a dict of environment variables to pass to Borg, and the borgmatic runtime
directory, collect the paths for any special files (character devices, block devices, and named
pipes / FIFOs) that Borg would encounter during a create. These are all paths that could cause
Borg to hang if its --read-special flag is used.
a working directory, and the borgmatic runtime directory, collect the paths for any special
files (character devices, block devices, and named pipes / FIFOs) that Borg would encounter
during a create. These are all paths that could cause Borg to hang if its --read-special flag is
used.
Skip looking for special files in the given borgmatic runtime directory, as borgmatic creates
its own special files there for database dumps. And if the borgmatic runtime directory is
@@ -144,7 +143,7 @@ def collect_special_file_paths(
+ ('--dry-run', '--list'),
capture_stderr=True,
working_directory=working_directory,
extra_environment=borg_environment,
extra_environment=environment.make_environment(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
@@ -299,7 +298,6 @@ def make_base_create_command(
logger.warning(
'Ignoring configured "read_special" value of false, as true is needed for database hooks.'
)
borg_environment = environment.make_environment(config)
working_directory = borgmatic.config.paths.get_working_directory(config)
logger.debug('Collecting special file paths')
@@ -309,7 +307,6 @@ def make_base_create_command(
config,
local_path,
working_directory,
borg_environment,
borgmatic_runtime_directory=borgmatic_runtime_directory,
)
@@ -396,8 +393,6 @@ def create_archive(
# the terminal directly.
output_file = DO_NOT_CAPTURE if progress else None
borg_environment = environment.make_environment(config)
create_flags += (
(('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ())
+ (('--stats',) if stats and not json and not dry_run else ())
@@ -414,7 +409,7 @@ def create_archive(
output_log_level,
output_file,
working_directory=working_directory,
extra_environment=borg_environment,
extra_environment=environment.make_environment(config),
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
@@ -422,7 +417,7 @@ def create_archive(
return execute_command_and_capture_output(
create_flags + create_positional_arguments,
working_directory=working_directory,
extra_environment=borg_environment,
extra_environment=environment.make_environment(config),
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
@@ -432,7 +427,7 @@ def create_archive(
output_log_level,
output_file,
working_directory=working_directory,
extra_environment=borg_environment,
extra_environment=environment.make_environment(config),
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
+10 -1
View File
@@ -1,6 +1,7 @@
import os
import borgmatic.borg.passcommand
import borgmatic.hooks.credential.parse
OPTION_TO_ENVIRONMENT_VARIABLE = {
'borg_base_directory': 'BORG_BASE_DIR',
@@ -14,6 +15,8 @@ OPTION_TO_ENVIRONMENT_VARIABLE = {
'temporary_directory': 'TMPDIR',
}
CREDENTIAL_OPTIONS = {'encryption_passphrase'}
DEFAULT_BOOL_OPTION_TO_DOWNCASE_ENVIRONMENT_VARIABLE = {
'relocated_repo_access_is_ok': 'BORG_RELOCATED_REPO_ACCESS_IS_OK',
'unknown_unencrypted_repo_access_is_ok': 'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK',
@@ -28,13 +31,19 @@ def make_environment(config):
'''
Given a borgmatic configuration dict, return its options converted to a Borg environment
variable dict.
Do not reuse this environment across multiple Borg invocations, because it can include
references to resources like anonymous pipes for passphrases—which can only be consumed once.
'''
environment = {}
for option_name, environment_variable_name in OPTION_TO_ENVIRONMENT_VARIABLE.items():
value = config.get(option_name)
if value:
if option_name in CREDENTIAL_OPTIONS and value is not None:
value = borgmatic.hooks.credential.parse.resolve_credential(value)
if value is not None:
environment[environment_variable_name] = str(value)
passphrase = borgmatic.borg.passcommand.get_passphrase_from_passcommand(config)
+4 -6
View File
@@ -44,7 +44,6 @@ def extract_last_archive_dry_run(
return
list_flag = ('--list',) if logger.isEnabledFor(logging.DEBUG) else ()
borg_environment = environment.make_environment(config)
full_extract_command = (
(local_path, 'extract', '--dry-run')
+ (('--remote-path', remote_path) if remote_path else ())
@@ -59,7 +58,7 @@ def extract_last_archive_dry_run(
execute_command(
full_extract_command,
extra_environment=borg_environment,
extra_environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
@@ -144,7 +143,6 @@ def extract_archive(
+ (tuple(paths) if paths else ())
)
borg_environment = environment.make_environment(config)
borg_exit_codes = config.get('borg_exit_codes')
full_destination_path = (
os.path.join(working_directory or '', destination_path) if destination_path else None
@@ -156,7 +154,7 @@ def extract_archive(
return execute_command(
full_command,
output_file=DO_NOT_CAPTURE,
extra_environment=borg_environment,
extra_environment=environment.make_environment(config),
working_directory=full_destination_path,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
@@ -168,7 +166,7 @@ def extract_archive(
full_command,
output_file=subprocess.PIPE,
run_to_completion=False,
extra_environment=borg_environment,
extra_environment=environment.make_environment(config),
working_directory=full_destination_path,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
@@ -178,7 +176,7 @@ def extract_archive(
# if the restore paths don't exist in the archive.
execute_command(
full_command,
extra_environment=borg_environment,
extra_environment=environment.make_environment(config),
working_directory=full_destination_path,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
+3 -6
View File
@@ -106,8 +106,6 @@ def capture_archive_listing(
format to use for the output, and local and remote Borg paths, capture the
output of listing that archive and return it as a list of file paths.
'''
borg_environment = environment.make_environment(config)
return tuple(
execute_command_and_capture_output(
make_list_command(
@@ -126,7 +124,7 @@ def capture_archive_listing(
local_path,
remote_path,
),
extra_environment=borg_environment,
extra_environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
@@ -194,7 +192,6 @@ def list_archive(
'The --json flag on the list action is not supported when using the --archive/--find flags.'
)
borg_environment = environment.make_environment(config)
borg_exit_codes = config.get('borg_exit_codes')
# If there are any paths to find (and there's not a single archive already selected), start by
@@ -224,7 +221,7 @@ def list_archive(
local_path,
remote_path,
),
extra_environment=borg_environment,
extra_environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
@@ -260,7 +257,7 @@ def list_archive(
execute_command(
main_command,
output_log_level=logging.ANSWER,
extra_environment=borg_environment,
extra_environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
+2 -3
View File
@@ -59,7 +59,6 @@ def mount_archive(
+ (tuple(mount_arguments.paths) if mount_arguments.paths else ())
)
borg_environment = environment.make_environment(config)
working_directory = borgmatic.config.paths.get_working_directory(config)
# Don't capture the output when foreground mode is used so that ctrl-C can work properly.
@@ -67,7 +66,7 @@ def mount_archive(
execute_command(
full_command,
output_file=DO_NOT_CAPTURE,
extra_environment=borg_environment,
extra_environment=environment.make_environment(config),
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
@@ -76,7 +75,7 @@ def mount_archive(
execute_command(
full_command,
extra_environment=borg_environment,
extra_environment=environment.make_environment(config),
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
+1 -1
View File
@@ -47,4 +47,4 @@ def get_passphrase_from_passcommand(config):
passphrase = config.get('encryption_passphrase')
working_directory = borgmatic.config.paths.get_working_directory(config)
return run_passcommand(passcommand, bool(passphrase), working_directory)
return run_passcommand(passcommand, bool(passphrase is not None), working_directory)
+2 -3
View File
@@ -50,14 +50,13 @@ def display_repository_info(
+ flags.make_repository_flags(repository_path, local_borg_version)
)
extra_environment = environment.make_environment(config)
working_directory = borgmatic.config.paths.get_working_directory(config)
borg_exit_codes = config.get('borg_exit_codes')
if repo_info_arguments.json:
return execute_command_and_capture_output(
full_command,
extra_environment=extra_environment,
extra_environment=environment.make_environment(config),
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
@@ -66,7 +65,7 @@ def display_repository_info(
execute_command(
full_command,
output_log_level=logging.ANSWER,
extra_environment=extra_environment,
extra_environment=environment.make_environment(config),
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
+2 -3
View File
@@ -140,7 +140,6 @@ def list_repository(
return JSON output).
'''
borgmatic.logger.add_custom_log_levels()
borg_environment = environment.make_environment(config)
main_command = make_repo_list_command(
repository_path,
@@ -165,7 +164,7 @@ def list_repository(
json_listing = execute_command_and_capture_output(
json_command,
extra_environment=borg_environment,
extra_environment=environment.make_environment(config),
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
@@ -179,7 +178,7 @@ def list_repository(
execute_command(
main_command,
output_log_level=logging.ANSWER,
extra_environment=borg_environment,
extra_environment=environment.make_environment(config),
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
+2 -2
View File
@@ -349,12 +349,12 @@ def make_parsers():
global_group.add_argument(
'--log-file-format',
type=str,
help='Log format string used for log messages written to the log file',
help='Python format string used for log messages written to the log file',
)
global_group.add_argument(
'--log-json',
action='store_true',
help='Write log messages and console output as one JSON object per log line instead of formatted text',
help='Write Borg log messages and console output as one JSON object per log line instead of formatted text',
)
global_group.add_argument(
'--override',
+18 -7
View File
@@ -43,6 +43,7 @@ from borgmatic.logger import (
DISABLED,
Log_prefix,
add_custom_log_levels,
configure_delayed_logging,
configure_logging,
should_do_markup,
)
@@ -534,13 +535,16 @@ def run_actions(
def load_configurations(config_filenames, overrides=None, resolve_env=True):
'''
Given a sequence of configuration filenames, load and validate each configuration file. Return
the results as a tuple of: dict of configuration filename to corresponding parsed configuration,
a sequence of paths for all loaded configuration files (including includes), and a sequence of
logging.LogRecord instances containing any parse errors.
Given a sequence of configuration filenames, a sequence of configuration file override strings
in the form of "option.suboption=value", and whether to resolve environment variables, load and
validate each configuration file. Return the results as a tuple of: dict of configuration
filename to corresponding parsed configuration, a sequence of paths for all loaded configuration
files (including includes), and a sequence of logging.LogRecord instances containing any parse
errors.
Log records are returned here instead of being logged directly because logging isn't yet
initialized at this point!
initialized at this point! (Although with the Delayed_logging_handler now in place, maybe this
approach could change.)
'''
# Dict mapping from config filename to corresponding parsed config dict.
configs = collections.OrderedDict()
@@ -562,7 +566,10 @@ def load_configurations(config_filenames, overrides=None, resolve_env=True):
)
try:
configs[config_filename], paths, parse_logs = validate.parse_configuration(
config_filename, validate.schema_filename(), overrides, resolve_env
config_filename,
validate.schema_filename(),
overrides,
resolve_env,
)
config_paths.update(paths)
logs.extend(parse_logs)
@@ -880,6 +887,7 @@ def exit_with_help_link(): # pragma: no cover
def main(extra_summary_logs=[]): # pragma: no cover
configure_signals()
configure_delayed_logging()
try:
arguments = parse_arguments(*sys.argv[1:])
@@ -905,9 +913,12 @@ def main(extra_summary_logs=[]): # pragma: no cover
print(borgmatic.commands.completion.fish.fish_completion())
sys.exit(0)
validate = bool('validate' in arguments)
config_filenames = tuple(collect.collect_config_filenames(global_arguments.config_paths))
configs, config_paths, parse_logs = load_configurations(
config_filenames, global_arguments.overrides, global_arguments.resolve_env
config_filenames,
global_arguments.overrides,
resolve_env=global_arguments.resolve_env and not validate,
)
configuration_parse_errors = (
(max(log.levelno for log in parse_logs) >= logging.CRITICAL) if parse_logs else False
+1 -1
View File
@@ -69,7 +69,7 @@ def include_configuration(loader, filename_node, include_directory, config_paths
]
raise ValueError(
'!include value is not supported; use a single filename or a list of filenames'
'The value given for the !include tag is invalid; use a single filename or a list of filenames instead'
)
+54 -34
View File
@@ -250,7 +250,7 @@ properties:
repositories that were initialized with passphrase/repokey/keyfile
encryption. Quote the value if it contains punctuation, so it parses
correctly. And backslash any quote or backslash literals as well.
Defaults to not set.
Defaults to not set. Supports the "{credential ...}" syntax.
example: "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
checkpoint_interval:
type: integer
@@ -632,8 +632,8 @@ properties:
long-running repository check into multiple
partial checks. Defaults to no interruption. Only
applies to the "repository" check, does not check
the repository index, and is not compatible with a
simultaneous "archives" check or "--repair" flag.
the repository index and is not compatible with
the "--repair" flag.
example: 3600
- required:
- name
@@ -989,13 +989,15 @@ properties:
Username with which to connect to the database. Defaults
to the username of the current user. You probably want
to specify the "postgres" superuser here when the
database name is "all".
database name is "all". Supports the "{credential ...}"
syntax.
example: dbuser
restore_username:
type: string
description: |
Username with which to restore the database. Defaults to
the "username" option.
the "username" option. Supports the "{credential ...}"
syntax.
example: dbuser
password:
type: string
@@ -1003,13 +1005,15 @@ properties:
Password with which to connect to the database. Omitting
a password will only work if PostgreSQL is configured to
trust the configured username without a password or you
create a ~/.pgpass file.
create a ~/.pgpass file. Supports the "{credential ...}"
syntax.
example: trustsome1
restore_password:
type: string
description: |
Password with which to connect to the restore database.
Defaults to the "password" option.
Defaults to the "password" option. Supports the
"{credential ...}" syntax.
example: trustsome1
no_owner:
type: boolean
@@ -1169,13 +1173,15 @@ properties:
type: string
description: |
Username with which to connect to the database. Defaults
to the username of the current user.
to the username of the current user. Supports the
"{credential ...}" syntax.
example: dbuser
restore_username:
type: string
description: |
Username with which to restore the database. Defaults to
the "username" option.
the "username" option. Supports the "{credential ...}"
syntax.
example: dbuser
password:
type: string
@@ -1183,6 +1189,14 @@ properties:
Password with which to connect to the database. Omitting
a password will only work if MariaDB is configured to
trust the configured username without a password.
Supports the "{credential ...}" syntax.
example: trustsome1
restore_password:
type: string
description: |
Password with which to connect to the restore database.
Defaults to the "password" option. Supports the
"{credential ...}" syntax.
example: trustsome1
mariadb_dump_command:
type: string
@@ -1201,12 +1215,6 @@ properties:
run a specific mariadb version (e.g., one inside a
running container). Defaults to "mariadb".
example: docker exec mariadb_container mariadb
restore_password:
type: string
description: |
Password with which to connect to the restore database.
Defaults to the "password" option.
example: trustsome1
format:
type: string
enum: ['sql']
@@ -1295,13 +1303,15 @@ properties:
type: string
description: |
Username with which to connect to the database. Defaults
to the username of the current user.
to the username of the current user. Supports the
"{credential ...}" syntax.
example: dbuser
restore_username:
type: string
description: |
Username with which to restore the database. Defaults to
the "username" option.
the "username" option. Supports the "{credential ...}"
syntax.
example: dbuser
password:
type: string
@@ -1309,12 +1319,14 @@ properties:
Password with which to connect to the database. Omitting
a password will only work if MySQL is configured to
trust the configured username without a password.
Supports the "{credential ...}" syntax.
example: trustsome1
restore_password:
type: string
description: |
Password with which to connect to the restore database.
Defaults to the "password" option.
Defaults to the "password" option. Supports the
"{credential ...}" syntax.
example: trustsome1
mysql_dump_command:
type: string
@@ -1451,25 +1463,29 @@ properties:
type: string
description: |
Username with which to connect to the database. Skip it
if no authentication is needed.
if no authentication is needed. Supports the
"{credential ...}" syntax.
example: dbuser
restore_username:
type: string
description: |
Username with which to restore the database. Defaults to
the "username" option.
the "username" option. Supports the "{credential ...}"
syntax.
example: dbuser
password:
type: string
description: |
Password with which to connect to the database. Skip it
if no authentication is needed.
if no authentication is needed. Supports the
"{credential ...}" syntax.
example: trustsome1
restore_password:
type: string
description: |
Password with which to connect to the restore database.
Defaults to the "password" option.
Defaults to the "password" option. Supports the
"{credential ...}" syntax.
example: trustsome1
authentication_database:
type: string
@@ -1528,18 +1544,20 @@ properties:
username:
type: string
description: |
The username used for authentication.
The username used for authentication. Supports the
"{credential ...}" syntax.
example: testuser
password:
type: string
description: |
The password used for authentication.
The password used for authentication. Supports the
"{credential ...}" syntax.
example: fakepassword
access_token:
type: string
description: |
An ntfy access token to authenticate with instead of
username/password.
username/password. Supports the "{credential ...}" syntax.
example: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
start:
type: object
@@ -1634,14 +1652,16 @@ properties:
token:
type: string
description: |
Your application's API token.
Your application's API token. Supports the "{credential
...}" syntax.
example: 7ms6TXHpTokTou2P6x4SodDeentHRa
user:
type: string
description: |
Your user/group key (or that of your target user), viewable
when logged into your dashboard: often referred to as
Your user/group key (or that of your target user), viewable
when logged into your dashboard: often referred to as
USER_KEY in Pushover documentation and code examples.
Supports the "{credential ...}" syntax.
example: hwRwoWsXMBWwgrSecfa9EfPey55WSN
start:
type: object
@@ -1915,19 +1935,19 @@ properties:
type: string
description: |
The username used for authentication. Not needed if using
an API key.
an API key. Supports the "{credential ...}" syntax.
example: testuser
password:
type: string
description: |
The password used for authentication. Not needed if using
an API key.
an API key. Supports the "{credential ...}" syntax.
example: fakepassword
api_key:
type: string
description: |
The API key used for authentication. Not needed if using
an username/password.
The API key used for authentication. Not needed if using an
username/password. Supports the "{credential ...}" syntax.
example: fakekey
start:
type: object
@@ -2207,8 +2227,8 @@ properties:
integration_key:
type: string
description: |
PagerDuty integration key used to notify PagerDuty
when a backup errors.
PagerDuty integration key used to notify PagerDuty when a
backup errors. Supports the "{credential ...}" syntax.
example: a177cad45bd374409f78906a810a3074
description: |
Configuration for a monitoring integration with PagerDuty. Create an
+4 -2
View File
@@ -88,8 +88,9 @@ def parse_configuration(config_filename, schema_filename, overrides=None, resolv
'''
Given the path to a config filename in YAML format, the path to a schema filename in a YAML
rendition of JSON Schema format, a sequence of configuration file override strings in the form
of "option.suboption=value", return the parsed configuration as a data structure of nested dicts
and lists corresponding to the schema. Example return value:
of "option.suboption=value", and whether to resolve environment variables, return the parsed
configuration as a data structure of nested dicts and lists corresponding to the schema. Example
return value:
{
'source_directories': ['/home', '/etc'],
@@ -124,6 +125,7 @@ def parse_configuration(config_filename, schema_filename, overrides=None, resolv
validator = jsonschema.Draft7Validator(schema)
except AttributeError: # pragma: no cover
validator = jsonschema.Draft4Validator(schema)
validation_errors = tuple(validator.iter_errors(config))
if validation_errors:
+42
View File
@@ -0,0 +1,42 @@
import functools
import re
import borgmatic.hooks.dispatch
IS_A_HOOK = False
CREDENTIAL_PATTERN = re.compile(
r'\{credential +(?P<hook_name>[A-Za-z0-9_]+) +(?P<credential_name>[A-Za-z0-9_]+)\}'
)
GENERAL_CREDENTIAL_PATTERN = re.compile(r'\{credential( +[^}]*)?\}')
@functools.cache
def resolve_credential(value):
'''
Given a configuration value containing a string like "{credential hookname credentialname}", resolve it by
calling the relevant hook to get the actual credential value. If the given value does not
actually contain a credential tag, then return it unchanged.
Cache the value so repeated calls to this function don't need to load the credential repeatedly.
Raise ValueError if the config could not be parsed or the credential could not be loaded.
'''
if value is None:
return value
result = CREDENTIAL_PATTERN.sub(
lambda matcher: borgmatic.hooks.dispatch.call_hook(
'load_credential', {}, matcher.group('hook_name'), matcher.group('credential_name')
),
value,
)
# If we've tried to parse the credential, but the parsed result still looks kind of like a
# credential, it means it's invalid syntax.
if GENERAL_CREDENTIAL_PATTERN.match(result):
raise ValueError(f'Cannot load credential with invalid syntax "{value}"')
return result
+35
View File
@@ -0,0 +1,35 @@
import logging
import os
import re
logger = logging.getLogger(__name__)
CREDENTIAL_NAME_PATTERN = re.compile(r'^\w+$')
def load_credential(hook_config, config, credential_name):
'''
Given the hook configuration dict, the configuration dict, and a credential name to load, read
the credential from the corresponding systemd credential file and return it.
Raise ValueError if the systemd CREDENTIALS_DIRECTORY environment variable is not set, the
credential name is invalid, or the credential file cannot be read.
'''
credentials_directory = os.environ.get('CREDENTIALS_DIRECTORY')
if not credentials_directory:
raise ValueError(
f'Cannot load credential "{credential_name}" because the systemd CREDENTIALS_DIRECTORY environment variable is not set'
)
if not CREDENTIAL_NAME_PATTERN.match(credential_name):
raise ValueError(f'Cannot load invalid credential name "{credential_name}"')
try:
with open(os.path.join(credentials_directory, credential_name)) as credential_file:
return credential_file.read().rstrip(os.linesep)
except (FileNotFoundError, OSError) as error:
logger.warning(error)
raise ValueError(f'Cannot load credential "{credential_name}" from file: {error.filename}')
+22 -7
View File
@@ -5,6 +5,7 @@ import shlex
import borgmatic.borg.pattern
import borgmatic.config.paths
import borgmatic.hooks.credential.parse
from borgmatic.execute import (
execute_command,
execute_command_and_capture_output,
@@ -45,7 +46,11 @@ def database_names_to_dump(database, extra_environment, dry_run):
+ (('--host', database['hostname']) if 'hostname' in database else ())
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
+ (('--user', database['username']) if 'username' in database else ())
+ (
('--user', borgmatic.hooks.credential.parse.resolve_credential(database['username']))
if 'username' in database
else ()
)
+ ('--skip-column-names', '--batch')
+ ('--execute', 'show schemas')
)
@@ -96,7 +101,11 @@ def execute_dump_command(
+ (('--host', database['hostname']) if 'hostname' in database else ())
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
+ (('--user', database['username']) if 'username' in database else ())
+ (
('--user', borgmatic.hooks.credential.parse.resolve_credential(database['username']))
if 'username' in database
else ()
)
+ ('--databases',)
+ database_names
+ ('--result-file', dump_filename)
@@ -152,7 +161,11 @@ def dump_data_sources(
for database in databases:
dump_path = make_dump_path(borgmatic_runtime_directory)
extra_environment = {'MYSQL_PWD': database['password']} if 'password' in database else None
extra_environment = (
{'MYSQL_PWD': borgmatic.hooks.credential.parse.resolve_credential(database['password'])}
if 'password' in database
else None
)
dump_database_names = database_names_to_dump(database, extra_environment, dry_run)
if not dump_database_names:
@@ -251,11 +264,13 @@ def restore_data_source_dump(
port = str(
connection_params['port'] or data_source.get('restore_port', data_source.get('port', ''))
)
username = connection_params['username'] or data_source.get(
'restore_username', data_source.get('username')
username = borgmatic.hooks.credential.parse.resolve_credential(
connection_params['username']
or data_source.get('restore_username', data_source.get('username'))
)
password = connection_params['password'] or data_source.get(
'restore_password', data_source.get('password')
password = borgmatic.hooks.credential.parse.resolve_credential(
connection_params['password']
or data_source.get('restore_password', data_source.get('password'))
)
mariadb_restore_command = tuple(
+25 -6
View File
@@ -4,6 +4,7 @@ import shlex
import borgmatic.borg.pattern
import borgmatic.config.paths
import borgmatic.hooks.credential.parse
from borgmatic.execute import execute_command, execute_command_with_processes
from borgmatic.hooks.data_source import dump
@@ -98,8 +99,26 @@ def build_dump_command(database, dump_filename, dump_format):
+ (('--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 ())
+ (('--username', shlex.quote(database['username'])) if 'username' in database else ())
+ (('--password', shlex.quote(database['password'])) if 'password' in database else ())
+ (
(
'--username',
shlex.quote(
borgmatic.hooks.credential.parse.resolve_credential(database['username'])
),
)
if 'username' in database
else ()
)
+ (
(
'--password',
shlex.quote(
borgmatic.hooks.credential.parse.resolve_credential(database['password'])
),
)
if 'password' in database
else ()
)
+ (
('--authenticationDatabase', shlex.quote(database['authentication_database']))
if 'authentication_database' in database
@@ -198,11 +217,11 @@ def build_restore_command(extract_process, database, dump_filename, connection_p
'restore_hostname', database.get('hostname')
)
port = str(connection_params['port'] or database.get('restore_port', database.get('port', '')))
username = connection_params['username'] or database.get(
'restore_username', database.get('username')
username = borgmatic.hooks.credential.parse.resolve_credential(
connection_params['username'] or database.get('restore_username', database.get('username'))
)
password = connection_params['password'] or database.get(
'restore_password', database.get('password')
password = borgmatic.hooks.credential.parse.resolve_credential(
connection_params['password'] or database.get('restore_password', database.get('password'))
)
command = ['mongorestore']
+22 -7
View File
@@ -5,6 +5,7 @@ import shlex
import borgmatic.borg.pattern
import borgmatic.config.paths
import borgmatic.hooks.credential.parse
from borgmatic.execute import (
execute_command,
execute_command_and_capture_output,
@@ -45,7 +46,11 @@ def database_names_to_dump(database, extra_environment, dry_run):
+ (('--host', database['hostname']) if 'hostname' in database else ())
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
+ (('--user', database['username']) if 'username' in database else ())
+ (
('--user', borgmatic.hooks.credential.parse.resolve_credential(database['username']))
if 'username' in database
else ()
)
+ ('--skip-column-names', '--batch')
+ ('--execute', 'show schemas')
)
@@ -95,7 +100,11 @@ def execute_dump_command(
+ (('--host', database['hostname']) if 'hostname' in database else ())
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
+ (('--user', database['username']) if 'username' in database else ())
+ (
('--user', borgmatic.hooks.credential.parse.resolve_credential(database['username']))
if 'username' in database
else ()
)
+ ('--databases',)
+ database_names
+ ('--result-file', dump_filename)
@@ -151,7 +160,11 @@ def dump_data_sources(
for database in databases:
dump_path = make_dump_path(borgmatic_runtime_directory)
extra_environment = {'MYSQL_PWD': database['password']} if 'password' in database else None
extra_environment = (
{'MYSQL_PWD': borgmatic.hooks.credential.parse.resolve_credential(database['password'])}
if 'password' in database
else None
)
dump_database_names = database_names_to_dump(database, extra_environment, dry_run)
if not dump_database_names:
@@ -250,11 +263,13 @@ def restore_data_source_dump(
port = str(
connection_params['port'] or data_source.get('restore_port', data_source.get('port', ''))
)
username = connection_params['username'] or data_source.get(
'restore_username', data_source.get('username')
username = borgmatic.hooks.credential.parse.resolve_credential(
connection_params['username']
or data_source.get('restore_username', data_source.get('username'))
)
password = connection_params['password'] or data_source.get(
'restore_password', data_source.get('password')
password = borgmatic.hooks.credential.parse.resolve_credential(
connection_params['password']
or data_source.get('restore_password', data_source.get('password'))
)
mysql_restore_command = tuple(
+26 -7
View File
@@ -7,6 +7,7 @@ import shlex
import borgmatic.borg.pattern
import borgmatic.config.paths
import borgmatic.hooks.credential.parse
from borgmatic.execute import (
execute_command,
execute_command_and_capture_output,
@@ -33,11 +34,14 @@ def make_extra_environment(database, restore_connection_params=None):
try:
if restore_connection_params:
extra['PGPASSWORD'] = restore_connection_params.get('password') or database.get(
'restore_password', database['password']
extra['PGPASSWORD'] = borgmatic.hooks.credential.parse.resolve_credential(
restore_connection_params.get('password')
or database.get('restore_password', database['password'])
)
else:
extra['PGPASSWORD'] = database['password']
extra['PGPASSWORD'] = borgmatic.hooks.credential.parse.resolve_credential(
database['password']
)
except (AttributeError, KeyError):
pass
@@ -82,7 +86,14 @@ def database_names_to_dump(database, extra_environment, dry_run):
+ ('--list', '--no-password', '--no-psqlrc', '--csv', '--tuples-only')
+ (('--host', database['hostname']) if 'hostname' in database else ())
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--username', database['username']) if 'username' in database else ())
+ (
(
'--username',
borgmatic.hooks.credential.parse.resolve_credential(database['username']),
)
if 'username' in database
else ()
)
+ (tuple(database['list_options'].split(' ')) if 'list_options' in database else ())
)
logger.debug('Querying for "all" PostgreSQL databases to dump')
@@ -174,7 +185,14 @@ def dump_data_sources(
+ (('--host', shlex.quote(database['hostname'])) if 'hostname' in database else ())
+ (('--port', shlex.quote(str(database['port']))) if 'port' in database else ())
+ (
('--username', shlex.quote(database['username']))
(
'--username',
shlex.quote(
borgmatic.hooks.credential.parse.resolve_credential(
database['username']
)
),
)
if 'username' in database
else ()
)
@@ -290,8 +308,9 @@ def restore_data_source_dump(
port = str(
connection_params['port'] or data_source.get('restore_port', data_source.get('port', ''))
)
username = connection_params['username'] or data_source.get(
'restore_username', data_source.get('username')
username = borgmatic.hooks.credential.parse.resolve_credential(
connection_params['username']
or data_source.get('restore_username', data_source.get('username'))
)
all_databases = bool(data_source['name'] == 'all')
+16 -3
View File
@@ -2,6 +2,8 @@ import logging
import requests
import borgmatic.hooks.credential.parse
logger = logging.getLogger(__name__)
@@ -47,9 +49,20 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
'X-Tags': state_config.get('tags'),
}
username = hook_config.get('username')
password = hook_config.get('password')
access_token = hook_config.get('access_token')
try:
username = borgmatic.hooks.credential.parse.resolve_credential(
hook_config.get('username')
)
password = borgmatic.hooks.credential.parse.resolve_credential(
hook_config.get('password')
)
access_token = borgmatic.hooks.credential.parse.resolve_credential(
hook_config.get('access_token')
)
except ValueError as error:
logger.warning(f'Ntfy credential error: {error}')
return
auth = None
if access_token is not None:
+10 -1
View File
@@ -5,6 +5,7 @@ import platform
import requests
import borgmatic.hooks.credential.parse
from borgmatic.hooks.monitoring import monitor
logger = logging.getLogger(__name__)
@@ -39,11 +40,19 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
if dry_run:
return
try:
integration_key = borgmatic.hooks.credential.parse.resolve_credential(
hook_config.get('integration_key')
)
except ValueError as error:
logger.warning(f'PagerDuty credential error: {error}')
return
hostname = platform.node()
local_timestamp = datetime.datetime.now(datetime.timezone.utc).astimezone().isoformat()
payload = json.dumps(
{
'routing_key': hook_config['integration_key'],
'routing_key': integration_key,
'event_action': 'trigger',
'payload': {
'summary': f'backup failed on {hostname}',
+8 -2
View File
@@ -2,6 +2,8 @@ import logging
import requests
import borgmatic.hooks.credential.parse
logger = logging.getLogger(__name__)
@@ -32,8 +34,12 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
state_config = hook_config.get(state.name.lower(), {})
token = hook_config.get('token')
user = hook_config.get('user')
try:
token = borgmatic.hooks.credential.parse.resolve_credential(hook_config.get('token'))
user = borgmatic.hooks.credential.parse.resolve_credential(hook_config.get('user'))
except ValueError as error:
logger.warning(f'Pushover credential error: {error}')
return
logger.info(f'Updating Pushover{dry_run_label}')
+10 -3
View File
@@ -2,6 +2,8 @@ import logging
import requests
import borgmatic.hooks.credential.parse
logger = logging.getLogger(__name__)
@@ -34,10 +36,15 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
},
)
try:
username = borgmatic.hooks.credential.parse.resolve_credential(hook_config.get('username'))
password = borgmatic.hooks.credential.parse.resolve_credential(hook_config.get('password'))
api_key = borgmatic.hooks.credential.parse.resolve_credential(hook_config.get('api_key'))
except ValueError as error:
logger.warning(f'Zabbix credential error: {error}')
return
server = hook_config.get('server')
username = hook_config.get('username')
password = hook_config.get('password')
api_key = hook_config.get('api_key')
itemid = hook_config.get('itemid')
host = hook_config.get('host')
key = hook_config.get('key')
+94 -25
View File
@@ -87,11 +87,16 @@ class Multi_stream_handler(logging.Handler):
handler.setLevel(level)
class Console_no_color_formatter(logging.Formatter):
def __init__(self, *args, **kwargs): # pragma: no cover
super(Console_no_color_formatter, self).__init__(
'{prefix}{message}', style='{', defaults={'prefix': ''}, *args, **kwargs
)
class Log_prefix_formatter(logging.Formatter):
def __init__(self, fmt='{prefix}{message}', style='{', *args, **kwargs): # pragma: no cover
self.prefix = None
super(Log_prefix_formatter, self).__init__(fmt=fmt, style=style, *args, **kwargs)
def format(self, record): # pragma: no cover
record.prefix = f'{self.prefix}: ' if self.prefix else ''
return super(Log_prefix_formatter, self).format(record)
class Color(enum.Enum):
@@ -105,8 +110,9 @@ class Color(enum.Enum):
class Console_color_formatter(logging.Formatter):
def __init__(self, *args, **kwargs):
self.prefix = None
super(Console_color_formatter, self).__init__(
'{prefix}{message}', style='{', defaults={'prefix': ''}, *args, **kwargs
'{prefix}{message}', style='{', *args, **kwargs
)
def format(self, record):
@@ -124,6 +130,7 @@ class Console_color_formatter(logging.Formatter):
.get(record.levelno)
.value
)
record.prefix = f'{self.prefix}: ' if self.prefix else ''
return color_text(color, super(Console_color_formatter, self).format(record))
@@ -188,28 +195,32 @@ def add_custom_log_levels(): # pragma: no cover
def get_log_prefix():
'''
Return the current log prefix from the defaults for the formatter on the first logging handler,
set by set_log_prefix(). Return None if no such prefix exists.
Return the current log prefix set by set_log_prefix(). Return None if no such prefix exists.
It would be a whole lot easier to use logger.Formatter(defaults=...) instead, but that argument
doesn't exist until Python 3.10+.
'''
try:
return next(
handler.formatter._style._defaults.get('prefix').rstrip().rstrip(':')
formatter = next(
handler.formatter
for handler in logging.getLogger().handlers
if handler.formatter
if hasattr(handler.formatter, 'prefix')
)
except (StopIteration, AttributeError):
except StopIteration:
return None
return formatter.prefix
def set_log_prefix(prefix):
'''
Given a log prefix as a string, set it into the defaults for the formatters on all logging
handlers. Note that this overwrites any existing defaults.
Given a log prefix as a string, set it into the each handler's formatter so that it can inject
the prefix into each logged record.
'''
for handler in logging.getLogger().handlers:
try:
handler.formatter._style._defaults = {'prefix': f'{prefix}: ' if prefix else ''}
except AttributeError:
pass
if handler.formatter and hasattr(handler.formatter, 'prefix'):
handler.formatter.prefix = prefix
class Log_prefix:
@@ -252,6 +263,67 @@ class Log_prefix:
set_log_prefix(self.original_prefix)
class Delayed_logging_handler(logging.handlers.BufferingHandler):
'''
A logging handler that buffers logs and doesn't flush them until explicitly flushed (after
target handlers are actually set). It's useful for holding onto messages logged before logging
is configured, ensuring those records eventually make their way to the relevant logging
handlers.
When flushing, don't forward log records to a target handler if the record's log level is below
that of the handler. This recreates the standard logging behavior of, say, logging.DEBUG records
getting suppressed if a handler's level is only set to logging.INFO.
'''
def __init__(self):
super(Delayed_logging_handler, self).__init__(capacity=0)
self.targets = None
def shouldFlush(self, record):
return self.targets is not None
def flush(self):
self.acquire()
try:
if not self.targets:
return
for record in self.buffer:
for target in self.targets:
if record.levelno >= target.level:
target.handle(record)
self.buffer.clear()
finally:
self.release()
def configure_delayed_logging(): # pragma: no cover
'''
Configure a delayed logging handler to buffer anything that gets logged until we're ready to
deal with it.
'''
logging.basicConfig(
level=logging.DEBUG,
handlers=[Delayed_logging_handler()],
)
def flush_delayed_logging(target_handlers):
'''
Flush any previously buffered logs to our "real" logging handlers.
'''
root_logger = logging.getLogger()
if root_logger.handlers and isinstance(root_logger.handlers[0], Delayed_logging_handler):
delayed_handler = root_logger.handlers[0]
delayed_handler.targets = target_handlers
delayed_handler.flush()
root_logger.removeHandler(delayed_handler)
def configure_logging(
console_log_level,
syslog_log_level=None,
@@ -296,10 +368,9 @@ def configure_logging(
if color_enabled:
console_handler.setFormatter(Console_color_formatter())
else:
console_handler.setFormatter(Console_no_color_formatter())
console_handler.setFormatter(Log_prefix_formatter())
console_handler.setLevel(console_log_level)
handlers = [console_handler]
if syslog_log_level != logging.DISABLED:
@@ -315,10 +386,8 @@ def configure_logging(
if syslog_path:
syslog_handler = logging.handlers.SysLogHandler(address=syslog_path)
syslog_handler.setFormatter(
logging.Formatter(
Log_prefix_formatter(
'borgmatic: {levelname} {prefix}{message}', # noqa: FS003
style='{',
defaults={'prefix': ''},
)
)
syslog_handler.setLevel(syslog_log_level)
@@ -327,15 +396,15 @@ def configure_logging(
if log_file and log_file_log_level != logging.DISABLED:
file_handler = logging.handlers.WatchedFileHandler(log_file)
file_handler.setFormatter(
logging.Formatter(
Log_prefix_formatter(
log_file_format or '[{asctime}] {levelname}: {prefix}{message}', # noqa: FS003
style='{',
defaults={'prefix': ''},
)
)
file_handler.setLevel(log_file_log_level)
handlers.append(file_handler)
flush_delayed_logging(handlers)
logging.basicConfig(
level=min(handler.level for handler in handlers),
handlers=handlers,
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="generator" content="{{ eleventy.generator }}">
<link rel="icon" href="docs/static/borgmatic.png" type="image/x-icon">
<link rel="icon" href="https://torsion.org/borgmatic/docs/static/borgmatic.png" type="image/x-icon">
<title>{{ subtitle + ' - ' if subtitle}}{{ title }}</title>
{%- set css %}
{% include 'index.css' %}
+68 -19
View File
@@ -50,52 +50,101 @@ once per borgmatic run.
### Using systemd service credentials
Borgmatic supports using encrypted [credentials](https://systemd.io/CREDENTIALS/).
borgmatic supports using encrypted [systemd
credentials](https://systemd.io/CREDENTIALS/). To use this feature, start by
saving your password as an encrypted credential to
`/etc/credstore.encrypted/borgmatic.pw`, e.g.,
Save your password as an encrypted credential to `/etc/credstore.encrypted/borgmatic.pw`, e.g.,
```
# systemd-ask-password -n | systemd-creds encrypt - /etc/credstore.encrypted/borgmatic.pw
```bash
systemd-ask-password -n | systemd-creds encrypt - /etc/credstore.encrypted/borgmatic.pw
```
Then uncomment or use the following in your configuration file:
Then use the following in your configuration file:
```yaml
encryption_passcommand: "cat ${CREDENTIALS_DIRECTORY}/borgmatic.pw"
encryption_passphrase: "{credential systemd borgmatic.pw}"
```
<span class="minilink minilink-addedin">Prior to version 1.9.10</span> You can
accomplish the same thing with this configuration:
```yaml
encryption_passcommand: cat ${CREDENTIALS_DIRECTORY}/borgmatic.pw
```
Note that the name `borgmatic.pw` is hardcoded in the systemd service file.
To use multiple different passwords, save them as encrypted credentials to `/etc/credstore.encrypted/borgmatic/`, e.g.,
The `{credential ...}` syntax works for several different options in a borgmatic
configuration file besides just `encryption_passphrase`. For instance, the
username, password, and API token options within database and monitoring hooks
support `{credential ...}`:
```yaml
postgresql_databases:
- name: invoices
username: postgres
password: "{credential systemd borgmatic_db1}"
```
# mkdir /etc/credstore.encrypted/borgmatic
# systemd-ask-password -n | systemd-creds encrypt --name=borgmatic_backupserver1 - /etc/credstore.encrypted/borgmatic/backupserver1
# systemd-ask-password -n | systemd-creds encrypt --name=borgmatic_pw2 - /etc/credstore.encrypted/borgmatic/pw2
For specifics about which options are supported, see the
[configuration
reference](https://torsion.org/borgmatic/docs/reference/configuration/).
To use these credentials, you'll need to modify the borgmatic systemd service
file to support loading multiple credentials (assuming you need to load more
than one or anything not named `borgmatic.pw`).
Start by saving each encrypted credentials to
`/etc/credstore.encrypted/borgmatic/`. E.g.,
```bash
mkdir /etc/credstore.encrypted/borgmatic
systemd-ask-password -n | systemd-creds encrypt --name=borgmatic_backupserver1 - /etc/credstore.encrypted/borgmatic/backupserver1
systemd-ask-password -n | systemd-creds encrypt --name=borgmatic_pw2 - /etc/credstore.encrypted/borgmatic/pw2
...
```
Ensure that the file names, (e.g. `backupserver1`) match the corresponding part of
the `--name` option *after* the underscore (_), and that the part *before*
Ensure that the file names, (e.g. `backupserver1`) match the corresponding part
of the `--name` option *after* the underscore (_), and that the part *before*
the underscore matches the directory name (e.g. `borgmatic`).
Then, uncomment the appropriate line in the systemd service file:
```
# systemctl edit borgmatic.service
systemctl edit borgmatic.service
...
# Load multiple encrypted credentials.
LoadCredentialEncrypted=borgmatic:/etc/credstore.encrypted/borgmatic/
```
Finally, use the following in your configuration file:
Finally, use something like the following in your borgmatic configuration file
for each option value you'd like to load from systemd:
```
encryption_passcommand: "cat ${CREDENTIALS_DIRECTORY}/borgmatic_backupserver1"
```yaml
encryption_passphrase: "{credential systemd borgmatic_backupserver1}"
```
Adjust `borgmatic_backupserver1` according to the name given to the credential
and the directory set in the service file.
<span class="minilink minilink-addedin">Prior to version 1.9.10</span> Use the
following instead, but only for the `encryption_passcommand` option and
not other options:
```yaml
encryption_passcommand: cat ${CREDENTIALS_DIRECTORY}/borgmatic_backupserver1
```
Adjust `borgmatic_backupserver1` according to the name of the credential and the
directory set in the service file.
Be aware that when using this systemd `{credential ...}` feature, you may no
longer be able to run certain borgmatic actions outside of the systemd service,
as the credentials are only available from within the context of that service.
So for instance, `borgmatic list` necessarily relies on the
`encryption_passphrase` in order to access the Borg repository, but `list`
shouldn't need to load any credentials for your database or monitoring hooks.
The one exception is `borgmatic config validate`, which doesn't actually load
any credentials and should continue working anywhere.
### Environment variable interpolation
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "borgmatic"
version = "1.9.9"
version = "1.9.10"
authors = [
{ name="Dan Helfman", email="witten@torsion.org" },
]
+1 -2
View File
@@ -18,8 +18,7 @@ if [ -z "$TEST_CONTAINER" ]; then
fi
apk add --no-cache python3 py3-pip borgbackup postgresql17-client mariadb-client mongodb-tools \
py3-mongo py3-regex py3-ruamel.yaml py3-ruamel.yaml.clib py3-tox py3-typed-ast py3-yaml bash \
sqlite fish
py3-mongo py3-regex py3-ruamel.yaml py3-ruamel.yaml.clib py3-tox py3-yaml bash sqlite fish
export COVERAGE_FILE=/tmp/.coverage
tox --workdir /tmp/.tox --sitepackages
-1
View File
@@ -30,4 +30,3 @@ regex
requests==2.32.2
ruamel.yaml>0.15.0
toml==0.10.2
typed-ast
@@ -0,0 +1,71 @@
import json
import os
import shutil
import subprocess
import sys
import tempfile
def generate_configuration(config_path, repository_path):
'''
Generate borgmatic configuration into a file at the config path, and update the defaults so as
to work for testing, including updating the source directories, injecting the given repository
path, and tacking on an encryption passphrase loaded from systemd.
'''
subprocess.check_call(f'borgmatic config generate --destination {config_path}'.split(' '))
config = (
open(config_path)
.read()
.replace('ssh://user@backupserver/./sourcehostname.borg', repository_path)
.replace('- path: /mnt/backup', '')
.replace('label: local', '')
.replace('- /home/user/path with spaces', '')
.replace('- /home', f'- {config_path}')
.replace('- /etc', '')
.replace('- /var/log/syslog*', '')
+ '\nencryption_passphrase: "{credential systemd mycredential}"'
)
config_file = open(config_path, 'w')
config_file.write(config)
config_file.close()
def test_borgmatic_command():
# Create a Borg repository.
temporary_directory = tempfile.mkdtemp()
repository_path = os.path.join(temporary_directory, 'test.borg')
extract_path = os.path.join(temporary_directory, 'extract')
original_working_directory = os.getcwd()
os.mkdir(extract_path)
os.chdir(extract_path)
try:
config_path = os.path.join(temporary_directory, 'test.yaml')
generate_configuration(config_path, repository_path)
credential_path = os.path.join(temporary_directory, 'mycredential')
with open(credential_path, 'w') as credential_file:
credential_file.write('test')
subprocess.check_call(
f'borgmatic -v 2 --config {config_path} repo-create --encryption repokey'.split(' '),
env=dict(os.environ, **{'CREDENTIALS_DIRECTORY': temporary_directory}),
)
# Run borgmatic to generate a backup archive, and then list it to make sure it exists.
subprocess.check_call(
f'borgmatic --config {config_path}'.split(' '),
env=dict(os.environ, **{'CREDENTIALS_DIRECTORY': temporary_directory}),
)
output = subprocess.check_output(
f'borgmatic --config {config_path} list --json'.split(' '),
env=dict(os.environ, **{'CREDENTIALS_DIRECTORY': temporary_directory}),
).decode(sys.stdout.encoding)
parsed_output = json.loads(output)
assert len(parsed_output) == 1
assert len(parsed_output[0]['archives']) == 1
finally:
os.chdir(original_working_directory)
shutil.rmtree(temporary_directory)
+201 -74
View File
@@ -8,13 +8,16 @@ from borgmatic.borg import check as module
from ..test_verbosity import insert_logging_mock
def insert_execute_command_mock(command, working_directory=None, borg_exit_codes=None):
def insert_execute_command_mock(
command, output_file=None, 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,
output_file=output_file,
extra_environment=None,
working_directory=working_directory,
borg_local_path=command[0],
@@ -250,11 +253,11 @@ def test_make_check_name_flags_without_archives_check_and_with_archive_filter_fl
assert flags == ('--repository-only',)
def test_make_check_name_flags_with_data_check_returns_flag_and_implies_archives():
def test_make_check_name_flags_with_archives_and_data_check_returns_verify_data_flag():
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
flags = module.make_check_name_flags({'data'}, ())
flags = module.make_check_name_flags({'archives', 'data'}, ())
assert flags == (
'--archives-only',
@@ -262,6 +265,15 @@ def test_make_check_name_flags_with_data_check_returns_flag_and_implies_archives
)
def test_make_check_name_flags_with_repository_and_data_check_returns_verify_data_flag():
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
flags = module.make_check_name_flags({'archives', 'data', 'repository'}, ())
assert flags == ('--verify-data',)
def test_make_check_name_flags_with_extract_omits_extract_flag():
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
@@ -271,21 +283,6 @@ def test_make_check_name_flags_with_extract_omits_extract_flag():
assert flags == ()
def test_make_check_name_flags_with_repository_and_data_checks_does_not_return_repository_only():
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
flags = module.make_check_name_flags(
{
'repository',
'data',
},
(),
)
assert flags == ('--verify-data',)
def test_get_repository_id_with_valid_json_does_not_raise():
config = {}
flexmock(module.repo_info).should_receive('display_repository_info').and_return(
@@ -336,7 +333,9 @@ def test_get_repository_id_with_missing_json_keys_raises():
def test_check_archives_with_progress_passes_through_to_borg():
config = {}
flexmock(module).should_receive('make_check_name_flags').and_return(())
flexmock(module).should_receive('make_check_name_flags').with_args(
{'repository'}, ()
).and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.environment).should_receive('make_environment')
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
@@ -369,7 +368,9 @@ def test_check_archives_with_progress_passes_through_to_borg():
def test_check_archives_with_repair_passes_through_to_borg():
config = {}
flexmock(module).should_receive('make_check_name_flags').and_return(())
flexmock(module).should_receive('make_check_name_flags').with_args(
{'repository'}, ()
).and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.environment).should_receive('make_environment')
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
@@ -402,12 +403,15 @@ def test_check_archives_with_repair_passes_through_to_borg():
def test_check_archives_with_max_duration_flag_passes_through_to_borg():
config = {}
flexmock(module).should_receive('make_check_name_flags').and_return(())
flexmock(module).should_receive('make_check_name_flags').with_args(
{'repository'}, ()
).and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
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(
('borg', 'check', '--max-duration', '33', 'repo'),
output_file=None,
extra_environment=None,
working_directory=None,
borg_local_path='borg',
@@ -432,37 +436,17 @@ def test_check_archives_with_max_duration_flag_passes_through_to_borg():
)
def test_check_archives_with_max_duration_flag_and_archives_check_errors():
config = {}
flexmock(module).should_receive('execute_command').never()
with pytest.raises(ValueError):
module.check_archives(
repository_path='repo',
config=config,
local_borg_version='1.2.3',
check_arguments=flexmock(
progress=None,
repair=None,
only_checks=None,
force=None,
match_archives=None,
max_duration=33,
),
global_arguments=flexmock(log_json=False),
checks={'repository', 'archives'},
archive_filter_flags=(),
)
def test_check_archives_with_max_duration_option_passes_through_to_borg():
config = {'checks': [{'name': 'repository', 'max_duration': 33}]}
flexmock(module).should_receive('make_check_name_flags').and_return(())
flexmock(module).should_receive('make_check_name_flags').with_args(
{'repository'}, ()
).and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
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(
('borg', 'check', '--max-duration', '33', 'repo'),
output_file=None,
extra_environment=None,
working_directory=None,
borg_local_path='borg',
@@ -487,37 +471,145 @@ def test_check_archives_with_max_duration_option_passes_through_to_borg():
)
def test_check_archives_with_max_duration_option_and_archives_check_errors():
config = {'checks': [{'name': 'repository', 'max_duration': 33}]}
flexmock(module).should_receive('execute_command').never()
def test_check_archives_with_max_duration_option_and_archives_check_runs_repository_check_separately():
config = {'checks': [{'name': 'repository', 'max_duration': 33}, {'name': 'archives'}]}
flexmock(module).should_receive('make_check_name_flags').with_args({'archives'}, ()).and_return(
('--archives-only',)
)
flexmock(module).should_receive('make_check_name_flags').with_args(
{'repository'}, ()
).and_return(('--repository-only',))
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg', 'check', '--archives-only', 'repo'))
insert_execute_command_mock(
('borg', 'check', '--max-duration', '33', '--repository-only', 'repo')
)
with pytest.raises(ValueError):
module.check_archives(
repository_path='repo',
config=config,
local_borg_version='1.2.3',
check_arguments=flexmock(
progress=None,
repair=None,
only_checks=None,
force=None,
match_archives=None,
max_duration=None,
),
global_arguments=flexmock(log_json=False),
checks={'repository', 'archives'},
archive_filter_flags=(),
)
module.check_archives(
repository_path='repo',
config=config,
local_borg_version='1.2.3',
check_arguments=flexmock(
progress=None,
repair=None,
only_checks=None,
force=None,
match_archives=None,
max_duration=None,
),
global_arguments=flexmock(log_json=False),
checks={'repository', 'archives'},
archive_filter_flags=(),
)
def test_check_archives_with_max_duration_flag_and_archives_check_runs_repository_check_separately():
config = {'checks': [{'name': 'repository'}, {'name': 'archives'}]}
flexmock(module).should_receive('make_check_name_flags').with_args({'archives'}, ()).and_return(
('--archives-only',)
)
flexmock(module).should_receive('make_check_name_flags').with_args(
{'repository'}, ()
).and_return(('--repository-only',))
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg', 'check', '--archives-only', 'repo'))
insert_execute_command_mock(
('borg', 'check', '--max-duration', '33', '--repository-only', 'repo')
)
module.check_archives(
repository_path='repo',
config=config,
local_borg_version='1.2.3',
check_arguments=flexmock(
progress=None,
repair=None,
only_checks=None,
force=None,
match_archives=None,
max_duration=33,
),
global_arguments=flexmock(log_json=False),
checks={'repository', 'archives'},
archive_filter_flags=(),
)
def test_check_archives_with_max_duration_option_and_data_check_runs_repository_check_separately():
config = {'checks': [{'name': 'repository', 'max_duration': 33}, {'name': 'data'}]}
flexmock(module).should_receive('make_check_name_flags').with_args(
{'data', 'archives'}, ()
).and_return(('--archives-only', '--verify-data'))
flexmock(module).should_receive('make_check_name_flags').with_args(
{'repository'}, ()
).and_return(('--repository-only',))
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg', 'check', '--archives-only', '--verify-data', 'repo'))
insert_execute_command_mock(
('borg', 'check', '--max-duration', '33', '--repository-only', 'repo')
)
module.check_archives(
repository_path='repo',
config=config,
local_borg_version='1.2.3',
check_arguments=flexmock(
progress=None,
repair=None,
only_checks=None,
force=None,
match_archives=None,
max_duration=None,
),
global_arguments=flexmock(log_json=False),
checks={'repository', 'data'},
archive_filter_flags=(),
)
def test_check_archives_with_max_duration_flag_and_data_check_runs_repository_check_separately():
config = {'checks': [{'name': 'repository'}, {'name': 'data'}]}
flexmock(module).should_receive('make_check_name_flags').with_args(
{'data', 'archives'}, ()
).and_return(('--archives-only', '--verify-data'))
flexmock(module).should_receive('make_check_name_flags').with_args(
{'repository'}, ()
).and_return(('--repository-only',))
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg', 'check', '--archives-only', '--verify-data', 'repo'))
insert_execute_command_mock(
('borg', 'check', '--max-duration', '33', '--repository-only', 'repo')
)
module.check_archives(
repository_path='repo',
config=config,
local_borg_version='1.2.3',
check_arguments=flexmock(
progress=None,
repair=None,
only_checks=None,
force=None,
match_archives=None,
max_duration=33,
),
global_arguments=flexmock(log_json=False),
checks={'repository', 'data'},
archive_filter_flags=(),
)
def test_check_archives_with_max_duration_flag_overrides_max_duration_option():
config = {'checks': [{'name': 'repository', 'max_duration': 33}]}
flexmock(module).should_receive('make_check_name_flags').and_return(())
flexmock(module).should_receive('make_check_name_flags').with_args(
{'repository'}, ()
).and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
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(
('borg', 'check', '--max-duration', '44', 'repo'),
output_file=None,
extra_environment=None,
working_directory=None,
borg_local_path='borg',
@@ -575,9 +667,37 @@ def test_check_archives_calls_borg_with_parameters(checks):
)
def test_check_archives_with_data_check_implies_archives_check_calls_borg_with_parameters():
config = {}
flexmock(module).should_receive('make_check_name_flags').with_args(
{'data', 'archives'}, ()
).and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg', 'check', 'repo'))
module.check_archives(
repository_path='repo',
config=config,
local_borg_version='1.2.3',
check_arguments=flexmock(
progress=None,
repair=None,
only_checks=None,
force=None,
match_archives=None,
max_duration=None,
),
global_arguments=flexmock(log_json=False),
checks={'data'},
archive_filter_flags=(),
)
def test_check_archives_with_log_info_passes_through_to_borg():
config = {}
flexmock(module).should_receive('make_check_name_flags').and_return(())
flexmock(module).should_receive('make_check_name_flags').with_args(
{'repository'}, ()
).and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_logging_mock(logging.INFO)
insert_execute_command_mock(('borg', 'check', '--info', 'repo'))
@@ -602,7 +722,9 @@ def test_check_archives_with_log_info_passes_through_to_borg():
def test_check_archives_with_log_debug_passes_through_to_borg():
config = {}
flexmock(module).should_receive('make_check_name_flags').and_return(())
flexmock(module).should_receive('make_check_name_flags').with_args(
{'repository'}, ()
).and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_logging_mock(logging.DEBUG)
insert_execute_command_mock(('borg', 'check', '--debug', '--show-rc', 'repo'))
@@ -806,7 +928,9 @@ def test_check_archives_with_retention_prefix():
def test_check_archives_with_extra_borg_options_passes_through_to_borg():
config = {'extra_borg_options': {'check': '--extra --options'}}
flexmock(module).should_receive('make_check_name_flags').and_return(())
flexmock(module).should_receive('make_check_name_flags').with_args(
{'repository'}, ()
).and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg', 'check', '--extra', '--options', 'repo'))
@@ -829,15 +953,16 @@ def test_check_archives_with_extra_borg_options_passes_through_to_borg():
def test_check_archives_with_match_archives_passes_through_to_borg():
config = {}
flexmock(module).should_receive('make_check_name_flags').and_return(
('--match-archives', 'foo-*')
)
config = {'checks': [{'name': 'archives'}]}
flexmock(module).should_receive('make_check_name_flags').with_args(
{'archives'}, object
).and_return(('--match-archives', 'foo-*'))
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
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(
('borg', 'check', '--match-archives', 'foo-*', 'repo'),
output_file=None,
extra_environment=None,
working_directory=None,
borg_local_path='borg',
@@ -864,7 +989,9 @@ def test_check_archives_with_match_archives_passes_through_to_borg():
def test_check_archives_calls_borg_with_working_directory():
config = {'working_directory': '/working/dir'}
flexmock(module).should_receive('make_check_name_flags').and_return(())
flexmock(module).should_receive('make_check_name_flags').with_args(
{'repository'}, ()
).and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.environment).should_receive('make_environment')
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
+6 -6
View File
@@ -185,6 +185,7 @@ def test_any_parent_directories_treats_unrelated_paths_as_non_match():
def test_collect_special_file_paths_parses_special_files_from_borg_dry_run_file_list():
flexmock(module.environment).should_receive('make_environment').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
'Processing files ...\n- /foo\n+ /bar\n- /baz'
)
@@ -198,12 +199,12 @@ def test_collect_special_file_paths_parses_special_files_from_borg_dry_run_file_
config={},
local_path=None,
working_directory=None,
borg_environment=None,
borgmatic_runtime_directory='/run/borgmatic',
) == ('/foo', '/bar', '/baz')
def test_collect_special_file_paths_skips_borgmatic_runtime_directory():
flexmock(module.environment).should_receive('make_environment').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
'+ /foo\n- /run/borgmatic/bar\n- /baz'
)
@@ -225,12 +226,12 @@ def test_collect_special_file_paths_skips_borgmatic_runtime_directory():
config={},
local_path=None,
working_directory=None,
borg_environment=None,
borgmatic_runtime_directory='/run/borgmatic',
) == ('/foo', '/baz')
def test_collect_special_file_paths_with_borgmatic_runtime_directory_missing_from_paths_output_errors():
flexmock(module.environment).should_receive('make_environment').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
'+ /foo\n- /bar\n- /baz'
)
@@ -245,12 +246,12 @@ def test_collect_special_file_paths_with_borgmatic_runtime_directory_missing_fro
config={},
local_path=None,
working_directory=None,
borg_environment=None,
borgmatic_runtime_directory='/run/borgmatic',
)
def test_collect_special_file_paths_with_dry_run_and_borgmatic_runtime_directory_missing_from_paths_output_does_not_raise():
flexmock(module.environment).should_receive('make_environment').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
'+ /foo\n- /bar\n- /baz'
)
@@ -264,12 +265,12 @@ def test_collect_special_file_paths_with_dry_run_and_borgmatic_runtime_directory
config={},
local_path=None,
working_directory=None,
borg_environment=None,
borgmatic_runtime_directory='/run/borgmatic',
) == ('/foo', '/bar', '/baz')
def test_collect_special_file_paths_excludes_non_special_files():
flexmock(module.environment).should_receive('make_environment').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
'+ /foo\n+ /bar\n+ /baz'
)
@@ -285,12 +286,12 @@ def test_collect_special_file_paths_excludes_non_special_files():
config={},
local_path=None,
working_directory=None,
borg_environment=None,
borgmatic_runtime_directory='/run/borgmatic',
) == ('/foo', '/baz')
def test_collect_special_file_paths_omits_exclude_no_dump_flag_from_command():
flexmock(module.environment).should_receive('make_environment').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'create', '--dry-run', '--list'),
capture_stderr=True,
@@ -309,7 +310,6 @@ def test_collect_special_file_paths_omits_exclude_no_dump_flag_from_command():
config={},
local_path='borg',
working_directory=None,
borg_environment=None,
borgmatic_runtime_directory='/run/borgmatic',
)
+19
View File
@@ -24,11 +24,30 @@ def test_make_environment_with_passphrase_should_set_environment():
).and_return(None)
flexmock(module.os).should_receive('pipe').never()
flexmock(module.os.environ).should_receive('get').and_return(None)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
environment = module.make_environment({'encryption_passphrase': 'pass'})
assert environment.get('BORG_PASSPHRASE') == 'pass'
def test_make_environment_with_credential_tag_passphrase_should_load_it_and_set_environment():
flexmock(module.borgmatic.borg.passcommand).should_receive(
'get_passphrase_from_passcommand'
).and_return(None)
flexmock(module.os).should_receive('pipe').never()
flexmock(module.os.environ).should_receive('get').and_return(None)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).with_args('{credential systemd pass}').and_return('pass')
environment = module.make_environment({'encryption_passphrase': '{credential systemd pass}'})
assert environment.get('BORG_PASSPHRASE') == 'pass'
def test_make_environment_with_ssh_command_should_set_environment():
flexmock(module.borgmatic.borg.passcommand).should_receive(
'get_passphrase_from_passcommand'
+37
View File
@@ -4,6 +4,7 @@ from borgmatic.borg import passcommand as module
def test_run_passcommand_with_passphrase_configured_bails():
module.run_passcommand.cache_clear()
flexmock(module.borgmatic.execute).should_receive('execute_command_and_capture_output').never()
assert (
@@ -13,6 +14,7 @@ def test_run_passcommand_with_passphrase_configured_bails():
def test_run_passcommand_without_passphrase_configured_executes_passcommand():
module.run_passcommand.cache_clear()
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).and_return('passphrase').once()
@@ -24,6 +26,7 @@ def test_run_passcommand_without_passphrase_configured_executes_passcommand():
def test_get_passphrase_from_passcommand_with_configured_passcommand_runs_it():
module.run_passcommand.cache_clear()
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
'/working'
)
@@ -40,6 +43,7 @@ def test_get_passphrase_from_passcommand_with_configured_passcommand_runs_it():
def test_get_passphrase_from_passcommand_with_configured_passphrase_and_passcommand_detects_passphrase():
module.run_passcommand.cache_clear()
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
'/working'
)
@@ -53,3 +57,36 @@ def test_get_passphrase_from_passcommand_with_configured_passphrase_and_passcomm
)
is None
)
def test_get_passphrase_from_passcommand_with_configured_blank_passphrase_and_passcommand_detects_passphrase():
module.run_passcommand.cache_clear()
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
'/working'
)
flexmock(module).should_receive('run_passcommand').with_args(
'command', True, '/working'
).and_return(None).once()
assert (
module.get_passphrase_from_passcommand(
{'encryption_passphrase': '', 'encryption_passcommand': 'command'},
)
is None
)
def test_run_passcommand_caches_passcommand_after_first_call():
module.run_passcommand.cache_clear()
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).and_return('passphrase').once()
assert (
module.run_passcommand('passcommand', passphrase_configured=False, working_directory=None)
== 'passphrase'
)
assert (
module.run_passcommand('passcommand', passphrase_configured=False, working_directory=None)
== 'passphrase'
)
+11 -2
View File
@@ -1114,7 +1114,11 @@ def test_run_actions_runs_multiple_actions_in_argument_order():
)
def test_load_configurations_collects_parsed_configurations_and_logs():
@pytest.mark.parametrize(
'resolve_env',
((True, False),),
)
def test_load_configurations_collects_parsed_configurations_and_logs(resolve_env):
configuration = flexmock()
other_configuration = flexmock()
test_expected_logs = [flexmock(), flexmock()]
@@ -1123,7 +1127,12 @@ def test_load_configurations_collects_parsed_configurations_and_logs():
configuration, ['/tmp/test.yaml'], test_expected_logs
).and_return(other_configuration, ['/tmp/other.yaml'], other_expected_logs)
configs, config_paths, logs = tuple(module.load_configurations(('test.yaml', 'other.yaml')))
configs, config_paths, logs = tuple(
module.load_configurations(
('test.yaml', 'other.yaml'),
resolve_env=resolve_env,
)
)
assert configs == {'test.yaml': configuration, 'other.yaml': other_configuration}
assert config_paths == ['/tmp/other.yaml', '/tmp/test.yaml']
+52
View File
@@ -0,0 +1,52 @@
import pytest
from flexmock import flexmock
from borgmatic.hooks.credential import parse as module
def test_resolve_credential_passes_through_string_without_credential_tag():
module.resolve_credential.cache_clear()
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').never()
assert module.resolve_credential('{no credentials here}') == '{no credentials here}'
def test_resolve_credential_passes_through_none():
module.resolve_credential.cache_clear()
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').never()
assert module.resolve_credential(None) is None
@pytest.mark.parametrize('invalid_value', ('{credential}', '{credential }', '{credential systemd}'))
def test_resolve_credential_with_invalid_credential_tag_raises(invalid_value):
module.resolve_credential.cache_clear()
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').never()
with pytest.raises(ValueError):
module.resolve_credential(invalid_value)
def test_resolve_credential_with_valid_credential_tag_loads_credential():
module.resolve_credential.cache_clear()
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').with_args(
'load_credential',
{},
'systemd',
'mycredential',
).and_return('result').once()
assert module.resolve_credential('{credential systemd mycredential}') == 'result'
def test_resolve_credential_caches_credential_after_first_call():
module.resolve_credential.cache_clear()
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').with_args(
'load_credential',
{},
'systemd',
'mycredential',
).and_return('result').once()
assert module.resolve_credential('{credential systemd mycredential}') == 'result'
assert module.resolve_credential('{credential systemd mycredential}') == 'result'
@@ -0,0 +1,51 @@
import io
import sys
import pytest
from flexmock import flexmock
from borgmatic.hooks.credential import systemd as module
def test_load_credential_without_credentials_directory_raises():
flexmock(module.os.environ).should_receive('get').with_args('CREDENTIALS_DIRECTORY').and_return(
None
)
with pytest.raises(ValueError):
module.load_credential(hook_config={}, config={}, credential_name='mycredential')
def test_load_credential_with_invalid_credential_name_raises():
flexmock(module.os.environ).should_receive('get').with_args('CREDENTIALS_DIRECTORY').and_return(
'/var'
)
with pytest.raises(ValueError):
module.load_credential(hook_config={}, config={}, credential_name='../../my!@#$credential')
def test_load_credential_reads_named_credential_from_file():
flexmock(module.os.environ).should_receive('get').with_args('CREDENTIALS_DIRECTORY').and_return(
'/var'
)
credential_stream = io.StringIO('password')
credential_stream.name = '/var/mycredential'
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('/var/mycredential').and_return(credential_stream)
assert (
module.load_credential(hook_config={}, config={}, credential_name='mycredential')
== 'password'
)
def test_load_credential_with_file_not_found_error_raises():
flexmock(module.os.environ).should_receive('get').with_args('CREDENTIALS_DIRECTORY').and_return(
'/var'
)
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('/var/mycredential').and_raise(FileNotFoundError)
with pytest.raises(ValueError):
module.load_credential(hook_config={}, config={}, credential_name='mycredential')
@@ -25,6 +25,9 @@ def test_database_names_to_dump_bails_for_dry_run():
def test_database_names_to_dump_queries_mariadb_for_database_names():
extra_environment = flexmock()
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('mariadb', '--skip-column-names', '--batch', '--execute', 'show schemas'),
extra_environment=extra_environment,
@@ -50,6 +53,9 @@ def test_dump_data_sources_dumps_each_database():
databases = [{'name': 'foo'}, {'name': 'bar'}]
processes = [flexmock(), flexmock()]
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
('bar',)
)
@@ -81,6 +87,9 @@ def test_dump_data_sources_dumps_with_password():
database = {'name': 'foo', 'username': 'root', 'password': 'trustsome1'}
process = flexmock()
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
('bar',)
)
@@ -108,6 +117,9 @@ def test_dump_data_sources_dumps_all_databases_at_once():
databases = [{'name': 'all'}]
process = flexmock()
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('database_names_to_dump').and_return(('foo', 'bar'))
flexmock(module).should_receive('execute_dump_command').with_args(
database={'name': 'all'},
@@ -132,6 +144,9 @@ def test_dump_data_sources_dumps_all_databases_separately_when_format_configured
databases = [{'name': 'all', 'format': 'sql'}]
processes = [flexmock(), flexmock()]
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('database_names_to_dump').and_return(('foo', 'bar'))
for name, process in zip(('foo', 'bar'), processes):
@@ -199,6 +214,9 @@ def test_execute_dump_command_runs_mariadb_dump():
process = flexmock()
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -231,6 +249,9 @@ def test_execute_dump_command_runs_mariadb_dump_without_add_drop_database():
process = flexmock()
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -262,6 +283,9 @@ def test_execute_dump_command_runs_mariadb_dump_with_hostname_and_port():
process = flexmock()
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -300,6 +324,9 @@ def test_execute_dump_command_runs_mariadb_dump_with_username_and_password():
process = flexmock()
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -334,6 +361,9 @@ def test_execute_dump_command_runs_mariadb_dump_with_options():
process = flexmock()
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -367,6 +397,9 @@ def test_execute_dump_command_runs_non_default_mariadb_dump_with_options():
process = flexmock()
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -422,6 +455,9 @@ def test_execute_dump_command_with_duplicate_dump_skips_mariadb_dump():
def test_execute_dump_command_with_dry_run_skips_mariadb_dump():
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').never()
@@ -442,6 +478,9 @@ def test_execute_dump_command_with_dry_run_skips_mariadb_dump():
def test_dump_data_sources_errors_for_missing_all_databases():
databases = [{'name': 'all'}]
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
'databases/localhost/all'
)
@@ -461,6 +500,9 @@ def test_dump_data_sources_errors_for_missing_all_databases():
def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run():
databases = [{'name': 'all'}]
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
'databases/localhost/all'
)
@@ -483,6 +525,9 @@ def test_restore_data_source_dump_runs_mariadb_to_restore():
hook_config = [{'name': 'foo'}, {'name': 'bar'}]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
('mariadb', '--batch'),
processes=[extract_process],
@@ -511,6 +556,9 @@ def test_restore_data_source_dump_runs_mariadb_with_options():
hook_config = [{'name': 'foo', 'restore_options': '--harder'}]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
('mariadb', '--batch', '--harder'),
processes=[extract_process],
@@ -541,6 +589,9 @@ def test_restore_data_source_dump_runs_non_default_mariadb_with_options():
]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
('custom_mariadb', '--batch', '--harder'),
processes=[extract_process],
@@ -569,6 +620,9 @@ def test_restore_data_source_dump_runs_mariadb_with_hostname_and_port():
hook_config = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mariadb',
@@ -606,6 +660,9 @@ def test_restore_data_source_dump_runs_mariadb_with_username_and_password():
hook_config = [{'name': 'foo', 'username': 'root', 'password': 'trustsome1'}]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
('mariadb', '--batch', '--user', 'root'),
processes=[extract_process],
@@ -644,6 +701,9 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mariadb',
@@ -695,6 +755,9 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mariadb',
@@ -733,6 +796,9 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
def test_restore_data_source_dump_with_dry_run_skips_restore():
hook_config = [{'name': 'foo'}]
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_with_processes').never()
module.restore_data_source_dump(
@@ -124,6 +124,9 @@ def test_dump_data_sources_runs_mongodump_with_username_and_password():
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
'databases/localhost/foo'
)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -242,6 +245,9 @@ def test_dump_data_sources_runs_mongodumpall_for_all_databases():
def test_build_dump_command_with_username_injection_attack_gets_escaped():
database = {'name': 'test', 'username': 'bob; naughty-command'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
command = module.build_dump_command(database, dump_filename='test', dump_format='archive')
@@ -254,6 +260,9 @@ def test_restore_data_source_dump_runs_mongorestore():
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: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
['mongorestore', '--archive', '--drop'],
processes=[extract_process],
@@ -285,6 +294,9 @@ def test_restore_data_source_dump_runs_mongorestore_with_hostname_and_port():
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: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
[
'mongorestore',
@@ -330,6 +342,9 @@ def test_restore_data_source_dump_runs_mongorestore_with_username_and_password()
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: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
[
'mongorestore',
@@ -381,6 +396,9 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
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: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
[
'mongorestore',
@@ -436,6 +454,9 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
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: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
[
'mongorestore',
@@ -479,6 +500,9 @@ def test_restore_data_source_dump_runs_mongorestore_with_options():
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: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
['mongorestore', '--archive', '--drop', '--harder'],
processes=[extract_process],
@@ -508,6 +532,9 @@ def test_restore_databases_dump_runs_mongorestore_with_schemas():
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: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
[
'mongorestore',
@@ -545,6 +572,9 @@ def test_restore_data_source_dump_runs_psql_for_all_database_dump():
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: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
['mongorestore', '--archive'],
processes=[extract_process],
@@ -573,6 +603,9 @@ def test_restore_data_source_dump_with_dry_run_skips_restore():
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: value)
flexmock(module).should_receive('execute_command_with_processes').never()
module.restore_data_source_dump(
@@ -596,6 +629,9 @@ def test_restore_data_source_dump_without_extract_process_restores_from_disk():
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('/dump/path')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
['mongorestore', '--dir', '/dump/path', '--drop'],
processes=[],
@@ -16,6 +16,9 @@ def test_database_names_to_dump_passes_through_name():
def test_database_names_to_dump_bails_for_dry_run():
extra_environment = flexmock()
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_and_capture_output').never()
names = module.database_names_to_dump({'name': 'all'}, extra_environment, dry_run=True)
@@ -25,6 +28,9 @@ def test_database_names_to_dump_bails_for_dry_run():
def test_database_names_to_dump_queries_mysql_for_database_names():
extra_environment = flexmock()
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('mysql', '--skip-column-names', '--batch', '--execute', 'show schemas'),
extra_environment=extra_environment,
@@ -81,6 +87,9 @@ def test_dump_data_sources_dumps_with_password():
database = {'name': 'foo', 'username': 'root', 'password': 'trustsome1'}
process = flexmock()
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
('bar',)
)
@@ -199,6 +208,9 @@ def test_execute_dump_command_runs_mysqldump():
process = flexmock()
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -231,6 +243,9 @@ def test_execute_dump_command_runs_mysqldump_without_add_drop_database():
process = flexmock()
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -262,6 +277,9 @@ def test_execute_dump_command_runs_mysqldump_with_hostname_and_port():
process = flexmock()
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -300,6 +318,9 @@ def test_execute_dump_command_runs_mysqldump_with_username_and_password():
process = flexmock()
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -334,6 +355,9 @@ def test_execute_dump_command_runs_mysqldump_with_options():
process = flexmock()
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -367,6 +391,9 @@ def test_execute_dump_command_runs_non_default_mysqldump():
process = flexmock()
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -420,6 +447,9 @@ def test_execute_dump_command_with_duplicate_dump_skips_mysqldump():
def test_execute_dump_command_with_dry_run_skips_mysqldump():
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').never()
@@ -440,6 +470,9 @@ def test_execute_dump_command_with_dry_run_skips_mysqldump():
def test_dump_data_sources_errors_for_missing_all_databases():
databases = [{'name': 'all'}]
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
'databases/localhost/all'
)
@@ -459,6 +492,9 @@ def test_dump_data_sources_errors_for_missing_all_databases():
def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run():
databases = [{'name': 'all'}]
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
'databases/localhost/all'
)
@@ -481,6 +517,9 @@ def test_restore_data_source_dump_runs_mysql_to_restore():
hook_config = [{'name': 'foo'}, {'name': 'bar'}]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
('mysql', '--batch'),
processes=[extract_process],
@@ -509,6 +548,9 @@ def test_restore_data_source_dump_runs_mysql_with_options():
hook_config = [{'name': 'foo', 'restore_options': '--harder'}]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
('mysql', '--batch', '--harder'),
processes=[extract_process],
@@ -537,6 +579,9 @@ def test_restore_data_source_dump_runs_non_default_mysql_with_options():
hook_config = [{'name': 'foo', 'mysql_command': 'custom_mysql', 'restore_options': '--harder'}]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
('custom_mysql', '--batch', '--harder'),
processes=[extract_process],
@@ -565,6 +610,9 @@ def test_restore_data_source_dump_runs_mysql_with_hostname_and_port():
hook_config = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mysql',
@@ -602,6 +650,9 @@ def test_restore_data_source_dump_runs_mysql_with_username_and_password():
hook_config = [{'name': 'foo', 'username': 'root', 'password': 'trustsome1'}]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
('mysql', '--batch', '--user', 'root'),
processes=[extract_process],
@@ -640,6 +691,9 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mysql',
@@ -691,6 +745,9 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mysql',
@@ -729,6 +786,9 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
def test_restore_data_source_dump_with_dry_run_skips_restore():
hook_config = [{'name': 'foo'}]
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_with_processes').never()
module.restore_data_source_dump(
@@ -24,6 +24,9 @@ def test_make_extra_environment_maps_options_to_environment():
'PGSSLROOTCERT': 'root.crt',
'PGSSLCRL': 'crl.crl',
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
extra_env = module.make_extra_environment(database)
@@ -32,6 +35,9 @@ def test_make_extra_environment_maps_options_to_environment():
def test_make_extra_environment_with_cli_password_sets_correct_password():
database = {'name': 'foo', 'restore_password': 'trustsome1', 'password': 'anotherpassword'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
extra = module.make_extra_environment(
database, restore_connection_params={'password': 'clipassword'}
@@ -78,6 +84,9 @@ def test_database_names_to_dump_passes_through_all_without_format():
def test_database_names_to_dump_with_all_and_format_and_dry_run_bails():
database = {'name': 'all', 'format': 'custom'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_and_capture_output').never()
assert module.database_names_to_dump(database, flexmock(), dry_run=True) == ()
@@ -85,6 +94,9 @@ def test_database_names_to_dump_with_all_and_format_and_dry_run_bails():
def test_database_names_to_dump_with_all_and_format_lists_databases():
database = {'name': 'all', 'format': 'custom'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
'foo,test,\nbar,test,"stuff and such"'
)
@@ -97,6 +109,9 @@ def test_database_names_to_dump_with_all_and_format_lists_databases():
def test_database_names_to_dump_with_all_and_format_lists_databases_with_hostname_and_port():
database = {'name': 'all', 'format': 'custom', 'hostname': 'localhost', 'port': 1234}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
(
'psql',
@@ -121,6 +136,9 @@ def test_database_names_to_dump_with_all_and_format_lists_databases_with_hostnam
def test_database_names_to_dump_with_all_and_format_lists_databases_with_username():
database = {'name': 'all', 'format': 'custom', 'username': 'postgres'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
(
'psql',
@@ -143,6 +161,9 @@ def test_database_names_to_dump_with_all_and_format_lists_databases_with_usernam
def test_database_names_to_dump_with_all_and_format_lists_databases_with_options():
database = {'name': 'all', 'format': 'custom', 'list_options': '--harder'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('psql', '--list', '--no-password', '--no-psqlrc', '--csv', '--tuples-only', '--harder'),
extra_environment=object,
@@ -156,6 +177,9 @@ def test_database_names_to_dump_with_all_and_format_lists_databases_with_options
def test_database_names_to_dump_with_all_and_format_excludes_particular_databases():
database = {'name': 'all', 'format': 'custom'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
'foo,test,\ntemplate0,test,blah'
)
@@ -169,6 +193,9 @@ def test_database_names_to_dump_with_all_and_psql_command_uses_custom_command():
'format': 'custom',
'psql_command': 'docker exec --workdir * mycontainer psql',
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
(
'docker',
@@ -219,6 +246,9 @@ def test_dump_data_sources_runs_pg_dump_for_each_database():
'databases/localhost/foo'
).and_return('databases/localhost/bar')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
for name, process in zip(('foo', 'bar'), processes):
@@ -323,6 +353,9 @@ def test_dump_data_sources_with_dry_run_skips_pg_dump():
'databases/localhost/foo'
).and_return('databases/localhost/bar')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
flexmock(module).should_receive('execute_command').never()
@@ -349,6 +382,9 @@ def test_dump_data_sources_runs_pg_dump_with_hostname_and_port():
'databases/database.example.org/foo'
)
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -394,6 +430,9 @@ def test_dump_data_sources_runs_pg_dump_with_username_and_password():
'databases/localhost/foo'
)
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -437,6 +476,9 @@ def test_dump_data_sources_with_username_injection_attack_gets_escaped():
'databases/localhost/foo'
)
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -477,6 +519,9 @@ def test_dump_data_sources_runs_pg_dump_with_directory_format():
'databases/localhost/foo'
)
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('create_parent_directory_for_dump')
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
@@ -519,6 +564,9 @@ def test_dump_data_sources_runs_pg_dump_with_options():
'databases/localhost/foo'
)
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -559,6 +607,9 @@ def test_dump_data_sources_runs_pg_dumpall_for_all_databases():
'databases/localhost/all'
)
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -588,6 +639,9 @@ def test_dump_data_sources_runs_non_default_pg_dump():
'databases/localhost/foo'
)
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -623,6 +677,9 @@ def test_restore_data_source_dump_runs_pg_restore():
hook_config = [{'name': 'foo', 'schemas': None}, {'name': 'bar'}]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_data_source_dump_filename')
@@ -677,6 +734,9 @@ def test_restore_data_source_dump_runs_pg_restore_with_hostname_and_port():
]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_data_source_dump_filename')
@@ -739,6 +799,9 @@ def test_restore_data_source_dump_runs_pg_restore_with_username_and_password():
]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('make_extra_environment').and_return(
{'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'}
)
@@ -810,6 +873,9 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('make_extra_environment').and_return(
{'PGPASSWORD': 'clipassword', 'PGSSLMODE': 'disable'}
)
@@ -889,6 +955,9 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('make_extra_environment').and_return(
{'PGPASSWORD': 'restorepassword', 'PGSSLMODE': 'disable'}
)
@@ -962,6 +1031,9 @@ def test_restore_data_source_dump_runs_pg_restore_with_options():
]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_data_source_dump_filename')
@@ -1016,6 +1088,9 @@ def test_restore_data_source_dump_runs_psql_for_all_database_dump():
hook_config = [{'name': 'all', 'schemas': None}]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_data_source_dump_filename')
@@ -1055,6 +1130,9 @@ def test_restore_data_source_dump_runs_psql_for_plain_database_dump():
hook_config = [{'name': 'foo', 'format': 'plain', 'schemas': None}]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_data_source_dump_filename')
@@ -1106,6 +1184,9 @@ def test_restore_data_source_dump_runs_non_default_pg_restore_and_psql():
]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_data_source_dump_filename')
@@ -1167,6 +1248,9 @@ def test_restore_data_source_dump_runs_non_default_pg_restore_and_psql():
def test_restore_data_source_dump_with_dry_run_skips_restore():
hook_config = [{'name': 'foo', 'schemas': None}]
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_data_source_dump_filename')
@@ -1191,6 +1275,9 @@ def test_restore_data_source_dump_with_dry_run_skips_restore():
def test_restore_data_source_dump_without_extract_process_restores_from_disk():
hook_config = [{'name': 'foo', 'schemas': None}]
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('/dump/path')
@@ -1243,6 +1330,9 @@ def test_restore_data_source_dump_without_extract_process_restores_from_disk():
def test_restore_data_source_dump_with_schemas_restores_schemas():
hook_config = [{'name': 'foo', 'schemas': ['bar', 'baz']}]
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('/dump/path')
+60
View File
@@ -36,6 +36,9 @@ def return_default_message_headers(state=Enum):
def test_ping_monitor_minimal_config_hits_hosted_ntfy_on_fail():
hook_config = {'topic': topic}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}',
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
@@ -57,6 +60,9 @@ def test_ping_monitor_with_access_token_hits_hosted_ntfy_on_fail():
'topic': topic,
'access_token': 'abc123',
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}',
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
@@ -80,6 +86,9 @@ def test_ping_monitor_with_username_password_and_access_token_ignores_username_p
'password': 'fakepassword',
'access_token': 'abc123',
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}',
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
@@ -103,6 +112,9 @@ def test_ping_monitor_with_username_password_hits_hosted_ntfy_on_fail():
'username': 'testuser',
'password': 'fakepassword',
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}',
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
@@ -121,6 +133,9 @@ def test_ping_monitor_with_username_password_hits_hosted_ntfy_on_fail():
def test_ping_monitor_with_password_but_no_username_warns():
hook_config = {'topic': topic, 'password': 'fakepassword'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}',
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
@@ -140,6 +155,9 @@ def test_ping_monitor_with_password_but_no_username_warns():
def test_ping_monitor_with_username_but_no_password_warns():
hook_config = {'topic': topic, 'username': 'testuser'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}',
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
@@ -159,6 +177,9 @@ def test_ping_monitor_with_username_but_no_password_warns():
def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_start():
hook_config = {'topic': topic}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.requests).should_receive('post').never()
module.ping_monitor(
@@ -173,6 +194,9 @@ def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_start():
def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_finish():
hook_config = {'topic': topic}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.requests).should_receive('post').never()
module.ping_monitor(
@@ -187,6 +211,9 @@ def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_finish():
def test_ping_monitor_minimal_config_hits_selfhosted_ntfy_on_fail():
hook_config = {'topic': topic, 'server': custom_base_url}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.requests).should_receive('post').with_args(
f'{custom_base_url}/{topic}',
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
@@ -205,6 +232,9 @@ def test_ping_monitor_minimal_config_hits_selfhosted_ntfy_on_fail():
def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_fail_dry_run():
hook_config = {'topic': topic}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.requests).should_receive('post').never()
module.ping_monitor(
@@ -219,6 +249,9 @@ def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_fail_dry_run():
def test_ping_monitor_custom_message_hits_hosted_ntfy_on_fail():
hook_config = {'topic': topic, 'fail': custom_message_config}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}', headers=custom_message_headers, auth=None
).and_return(flexmock(ok=True)).once()
@@ -235,6 +268,9 @@ def test_ping_monitor_custom_message_hits_hosted_ntfy_on_fail():
def test_ping_monitor_custom_state_hits_hosted_ntfy_on_start():
hook_config = {'topic': topic, 'states': ['start', 'fail']}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}',
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.START),
@@ -253,6 +289,9 @@ def test_ping_monitor_custom_state_hits_hosted_ntfy_on_start():
def test_ping_monitor_with_connection_error_logs_warning():
hook_config = {'topic': topic}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}',
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
@@ -270,8 +309,29 @@ def test_ping_monitor_with_connection_error_logs_warning():
)
def test_ping_monitor_with_credential_error_logs_warning():
hook_config = {'topic': topic}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).and_raise(ValueError)
flexmock(module.requests).should_receive('post').never()
flexmock(module.logger).should_receive('warning').once()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_with_other_error_logs_warning():
hook_config = {'topic': topic}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
response = flexmock(ok=False)
response.should_receive('raise_for_status').and_raise(
module.requests.exceptions.RequestException
@@ -4,6 +4,9 @@ from borgmatic.hooks.monitoring import pagerduty as module
def test_ping_monitor_ignores_start_state():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.requests).should_receive('post').never()
module.ping_monitor(
@@ -17,6 +20,9 @@ def test_ping_monitor_ignores_start_state():
def test_ping_monitor_ignores_finish_state():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.requests).should_receive('post').never()
module.ping_monitor(
@@ -30,6 +36,9 @@ def test_ping_monitor_ignores_finish_state():
def test_ping_monitor_calls_api_for_fail_state():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.requests).should_receive('post').and_return(flexmock(ok=True))
module.ping_monitor(
@@ -43,6 +52,9 @@ def test_ping_monitor_calls_api_for_fail_state():
def test_ping_monitor_dry_run_does_not_call_api():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.requests).should_receive('post').never()
module.ping_monitor(
@@ -56,6 +68,9 @@ def test_ping_monitor_dry_run_does_not_call_api():
def test_ping_monitor_with_connection_error_logs_warning():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.requests).should_receive('post').and_raise(
module.requests.exceptions.ConnectionError
)
@@ -71,8 +86,28 @@ def test_ping_monitor_with_connection_error_logs_warning():
)
def test_ping_monitor_with_credential_error_logs_warning():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).and_raise(ValueError)
flexmock(module.requests).should_receive('post').never()
flexmock(module.logger).should_receive('warning')
module.ping_monitor(
{'integration_key': 'abc123'},
{},
'config.yaml',
module.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_with_other_error_logs_warning():
response = flexmock(ok=False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
response.should_receive('raise_for_status').and_raise(
module.requests.exceptions.RequestException
)
+66 -2
View File
@@ -11,6 +11,9 @@ def test_ping_monitor_config_with_minimum_config_fail_state_backup_successfully_
should be auto populated with the default value which is the state name.
'''
hook_config = {'token': 'ksdjfwoweijfvwoeifvjmwghagy92', 'user': '983hfe0of902lkjfa2amanfgui'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').with_args(
'https://api.pushover.net/1/messages.json',
@@ -38,6 +41,9 @@ def test_ping_monitor_config_with_minimum_config_start_state_backup_not_send_to_
'start' state. Only the 'fail' state is enabled by default.
'''
hook_config = {'token': 'ksdjfwoweijfvwoeifvjmwghagy92', 'user': '983hfe0of902lkjfa2amanfgui'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').never()
@@ -63,6 +69,9 @@ def test_ping_monitor_start_state_backup_default_message_successfully_send_to_pu
'user': '983hfe0of902lkjfa2amanfgui',
'states': {'start', 'fail', 'finish'},
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').with_args(
'https://api.pushover.net/1/messages.json',
@@ -96,6 +105,9 @@ def test_ping_monitor_start_state_backup_custom_message_successfully_send_to_pus
'states': {'start', 'fail', 'finish'},
'start': {'message': 'custom start message'},
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').with_args(
'https://api.pushover.net/1/messages.json',
@@ -128,6 +140,9 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_emergency
'states': {'start', 'fail', 'finish'},
'start': {'priority': 2},
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').with_args(
'https://api.pushover.net/1/messages.json',
@@ -163,6 +178,9 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_emergency
'states': {'start', 'fail', 'finish'},
'start': {'priority': 2, 'expire': 600},
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').with_args(
'https://api.pushover.net/1/messages.json',
@@ -198,6 +216,9 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_emergency
'states': {'start', 'fail', 'finish'},
'start': {'priority': 2, 'retry': 30},
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').with_args(
'https://api.pushover.net/1/messages.json',
@@ -236,6 +257,9 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_high_decl
'start': {'priority': 1, 'expire': 30, 'retry': 30},
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').never()
@@ -288,6 +312,9 @@ def test_ping_monitor_start_state_backup_based_on_documentation_advanced_example
'url_title': 'Login to ticketing system',
},
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').with_args(
'https://api.pushover.net/1/messages.json',
@@ -351,6 +378,9 @@ def test_ping_monitor_fail_state_backup_based_on_documentation_advanced_example_
'url_title': 'Login to ticketing system',
},
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').with_args(
'https://api.pushover.net/1/messages.json',
@@ -419,6 +449,9 @@ def test_ping_monitor_finish_state_backup_based_on_documentation_advanced_exampl
'url_title': 'Login to ticketing system',
},
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').with_args(
'https://api.pushover.net/1/messages.json',
@@ -446,12 +479,15 @@ def test_ping_monitor_finish_state_backup_based_on_documentation_advanced_exampl
)
def test_ping_monitor_config_with_minimum_config_fail_state_backup_successfully_send_to_pushover_dryrun():
def test_ping_monitor_config_with_minimum_config_fail_state_backup_successfully_send_to_pushover_dry_run():
'''
This test should be the minimum working configuration. The "message"
should be auto populated with the default value which is the state name.
'''
hook_config = {'token': 'ksdjfwoweijfvwoeifvjmwghagy92', 'user': '983hfe0of902lkjfa2amanfgui'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').and_return(flexmock(ok=True)).never()
@@ -473,6 +509,9 @@ def test_ping_monitor_config_incorrect_state_exit_early():
'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
'user': '983hfe0of902lkjfa2amanfgui',
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').and_return(flexmock(ok=True)).never()
@@ -486,7 +525,7 @@ def test_ping_monitor_config_incorrect_state_exit_early():
)
def test_ping_monitor_push_post_error_exits_early():
def test_ping_monitor_push_post_error_bails():
'''
This test simulates the Pushover servers not responding with a 200 OK. We
should raise for status and warn then exit.
@@ -496,6 +535,9 @@ def test_ping_monitor_push_post_error_exits_early():
'user': '983hfe0of902lkjfa2amanfgui',
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
push_response = flexmock(ok=False)
push_response.should_receive('raise_for_status').and_raise(
module.requests.ConnectionError
@@ -520,3 +562,25 @@ def test_ping_monitor_push_post_error_exits_early():
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_credential_error_bails():
hook_config = hook_config = {
'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
'user': '983hfe0of902lkjfa2amanfgui',
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).and_raise(ValueError)
flexmock(module.requests).should_receive('post').never()
flexmock(module.logger).should_receive('warning').once()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
+88 -15
View File
@@ -57,7 +57,7 @@ AUTH_HEADERS_API_KEY = {
AUTH_HEADERS_USERNAME_PASSWORD = {'Content-Type': 'application/json-rpc'}
def test_ping_monitor_with_non_matching_state_exits_early():
def test_ping_monitor_with_non_matching_state_bails():
hook_config = {'api_key': API_KEY}
flexmock(module.requests).should_receive('post').never()
@@ -71,10 +71,13 @@ def test_ping_monitor_with_non_matching_state_exits_early():
)
def test_ping_monitor_config_with_api_key_only_exit_early():
def test_ping_monitor_config_with_api_key_only_bails():
# This test should exit early since only providing an API KEY is not enough
# for the hook to work
hook_config = {'api_key': API_KEY}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
@@ -88,10 +91,13 @@ def test_ping_monitor_config_with_api_key_only_exit_early():
)
def test_ping_monitor_config_with_host_only_exit_early():
def test_ping_monitor_config_with_host_only_bails():
# This test should exit early since only providing a HOST is not enough
# for the hook to work
hook_config = {'host': HOST}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
@@ -105,10 +111,13 @@ def test_ping_monitor_config_with_host_only_exit_early():
)
def test_ping_monitor_config_with_key_only_exit_early():
def test_ping_monitor_config_with_key_only_bails():
# This test should exit early since only providing a KEY is not enough
# for the hook to work
hook_config = {'key': KEY}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
@@ -122,10 +131,13 @@ def test_ping_monitor_config_with_key_only_exit_early():
)
def test_ping_monitor_config_with_server_only_exit_early():
def test_ping_monitor_config_with_server_only_bails():
# This test should exit early since only providing a SERVER is not enough
# for the hook to work
hook_config = {'server': SERVER}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
@@ -139,9 +151,12 @@ def test_ping_monitor_config_with_server_only_exit_early():
)
def test_ping_monitor_config_user_password_no_zabbix_data_exit_early():
def test_ping_monitor_config_user_password_no_zabbix_data_bails():
# This test should exit early since there are HOST/KEY or ITEMID provided to publish data to
hook_config = {'server': SERVER, 'username': USERNAME, 'password': PASSWORD}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
@@ -155,9 +170,12 @@ def test_ping_monitor_config_user_password_no_zabbix_data_exit_early():
)
def test_ping_monitor_config_api_key_no_zabbix_data_exit_early():
def test_ping_monitor_config_api_key_no_zabbix_data_bails():
# This test should exit early since there are HOST/KEY or ITEMID provided to publish data to
hook_config = {'server': SERVER, 'api_key': API_KEY}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
@@ -171,10 +189,13 @@ def test_ping_monitor_config_api_key_no_zabbix_data_exit_early():
)
def test_ping_monitor_config_itemid_no_auth_data_exit_early():
def test_ping_monitor_config_itemid_no_auth_data_bails():
# This test should exit early since there is no authentication provided
# and Zabbix requires authentication to use it's API
hook_config = {'server': SERVER, 'itemid': ITEMID}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
@@ -188,10 +209,13 @@ def test_ping_monitor_config_itemid_no_auth_data_exit_early():
)
def test_ping_monitor_config_host_and_key_no_auth_data_exit_early():
def test_ping_monitor_config_host_and_key_no_auth_data_bails():
# This test should exit early since there is no authentication provided
# and Zabbix requires authentication to use it's API
hook_config = {'server': SERVER, 'host': HOST, 'key': KEY}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
@@ -209,6 +233,9 @@ def test_ping_monitor_config_host_and_key_with_api_key_auth_data_successful():
# This test should simulate a successful POST to a Zabbix server. This test uses API_KEY
# to authenticate and HOST/KEY to know which item to populate in Zabbix.
hook_config = {'server': SERVER, 'host': HOST, 'key': KEY, 'api_key': API_KEY}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.requests).should_receive('post').with_args(
f'{SERVER}',
headers=AUTH_HEADERS_API_KEY,
@@ -226,8 +253,11 @@ def test_ping_monitor_config_host_and_key_with_api_key_auth_data_successful():
)
def test_ping_monitor_config_host_and_missing_key_exits_early():
def test_ping_monitor_config_host_and_missing_key_bails():
hook_config = {'server': SERVER, 'host': HOST, 'api_key': API_KEY}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
@@ -241,8 +271,11 @@ def test_ping_monitor_config_host_and_missing_key_exits_early():
)
def test_ping_monitor_config_key_and_missing_host_exits_early():
def test_ping_monitor_config_key_and_missing_host_bails():
hook_config = {'server': SERVER, 'key': KEY, 'api_key': API_KEY}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
@@ -267,6 +300,9 @@ def test_ping_monitor_config_host_and_key_with_username_password_auth_data_succe
'password': PASSWORD,
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
auth_response = flexmock(ok=True)
auth_response.should_receive('json').and_return(
{'jsonrpc': '2.0', 'result': '3fe6ed01a69ebd79907a120bcd04e494', 'id': 1}
@@ -296,7 +332,7 @@ def test_ping_monitor_config_host_and_key_with_username_password_auth_data_succe
)
def test_ping_monitor_config_host_and_key_with_username_password_auth_data_and_auth_post_error_exits_early():
def test_ping_monitor_config_host_and_key_with_username_password_auth_data_and_auth_post_error_bails():
hook_config = {
'server': SERVER,
'host': HOST,
@@ -305,6 +341,9 @@ def test_ping_monitor_config_host_and_key_with_username_password_auth_data_and_a
'password': PASSWORD,
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
auth_response = flexmock(ok=False)
auth_response.should_receive('json').and_return(
{'jsonrpc': '2.0', 'result': '3fe6ed01a69ebd79907a120bcd04e494', 'id': 1}
@@ -335,7 +374,7 @@ def test_ping_monitor_config_host_and_key_with_username_password_auth_data_and_a
)
def test_ping_monitor_config_host_and_key_with_username_and_missing_password_exits_early():
def test_ping_monitor_config_host_and_key_with_username_and_missing_password_bails():
hook_config = {
'server': SERVER,
'host': HOST,
@@ -343,6 +382,9 @@ def test_ping_monitor_config_host_and_key_with_username_and_missing_password_exi
'username': USERNAME,
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
@@ -356,7 +398,7 @@ def test_ping_monitor_config_host_and_key_with_username_and_missing_password_exi
)
def test_ping_monitor_config_host_and_key_with_passing_and_missing_username_exits_early():
def test_ping_monitor_config_host_and_key_with_password_and_missing_username_bails():
hook_config = {
'server': SERVER,
'host': HOST,
@@ -364,6 +406,9 @@ def test_ping_monitor_config_host_and_key_with_passing_and_missing_username_exit
'password': PASSWORD,
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
@@ -381,6 +426,9 @@ def test_ping_monitor_config_itemid_with_api_key_auth_data_successful():
# This test should simulate a successful POST to a Zabbix server. This test uses API_KEY
# to authenticate and HOST/KEY to know which item to populate in Zabbix.
hook_config = {'server': SERVER, 'itemid': ITEMID, 'api_key': API_KEY}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.requests).should_receive('post').with_args(
f'{SERVER}',
headers=AUTH_HEADERS_API_KEY,
@@ -403,6 +451,9 @@ def test_ping_monitor_config_itemid_with_username_password_auth_data_successful(
# to authenticate and HOST/KEY to know which item to populate in Zabbix.
hook_config = {'server': SERVER, 'itemid': ITEMID, 'username': USERNAME, 'password': PASSWORD}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
auth_response = flexmock(ok=True)
auth_response.should_receive('json').and_return(
{'jsonrpc': '2.0', 'result': '3fe6ed01a69ebd79907a120bcd04e494', 'id': 1}
@@ -432,9 +483,12 @@ def test_ping_monitor_config_itemid_with_username_password_auth_data_successful(
)
def test_ping_monitor_config_itemid_with_username_password_auth_data_and_push_post_error_exits_early():
def test_ping_monitor_config_itemid_with_username_password_auth_data_and_push_post_error_bails():
hook_config = {'server': SERVER, 'itemid': ITEMID, 'username': USERNAME, 'password': PASSWORD}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
auth_response = flexmock(ok=True)
auth_response.should_receive('json').and_return(
{'jsonrpc': '2.0', 'result': '3fe6ed01a69ebd79907a120bcd04e494', 'id': 1}
@@ -466,3 +520,22 @@ def test_ping_monitor_config_itemid_with_username_password_auth_data_and_push_po
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_with_credential_error_bails():
hook_config = {'server': SERVER, 'itemid': ITEMID, 'username': USERNAME, 'password': PASSWORD}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).and_raise(ValueError)
flexmock(module.requests).should_receive('post').never()
flexmock(module.logger).should_receive('warning').once()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
+118 -33
View File
@@ -228,16 +228,12 @@ def test_add_logging_level_skips_global_setting_if_already_set():
module.add_logging_level('PLAID', 99)
def test_get_log_prefix_gets_prefix_from_first_handler():
def test_get_log_prefix_gets_prefix_from_first_handler_formatter_with_prefix():
flexmock(module.logging).should_receive('getLogger').and_return(
flexmock(
handlers=[
flexmock(
formatter=flexmock(
_style=flexmock(_defaults=flexmock(get=lambda name: 'myprefix: '))
)
),
flexmock(),
flexmock(formatter=flexmock()),
flexmock(formatter=flexmock(prefix='myprefix')),
],
removeHandler=lambda handler: None,
)
@@ -261,8 +257,8 @@ def test_get_log_prefix_with_no_formatters_does_not_raise():
flexmock(module.logging).should_receive('getLogger').and_return(
flexmock(
handlers=[
flexmock(),
flexmock(),
flexmock(formatter=None),
flexmock(formatter=None),
],
removeHandler=lambda handler: None,
)
@@ -276,9 +272,8 @@ def test_get_log_prefix_with_no_prefix_does_not_raise():
flexmock(
handlers=[
flexmock(
formatter=flexmock(_style=flexmock(_defaults=flexmock(get=lambda name: None)))
formatter=flexmock(),
),
flexmock(),
],
removeHandler=lambda handler: None,
)
@@ -287,24 +282,20 @@ def test_get_log_prefix_with_no_prefix_does_not_raise():
assert module.get_log_prefix() is None
def test_set_log_prefix_updates_all_handlers():
styles = (
flexmock(_defaults=None),
flexmock(_defaults=None),
def test_set_log_prefix_updates_all_handler_formatters():
formatters = (
flexmock(prefix=None),
flexmock(prefix=None),
)
flexmock(module.logging).should_receive('getLogger').and_return(
flexmock(
handlers=[
flexmock(
formatter=flexmock(
_style=styles[0],
)
formatter=formatters[0],
),
flexmock(
formatter=flexmock(
_style=styles[1],
)
formatter=formatters[1],
),
],
removeHandler=lambda handler: None,
@@ -313,12 +304,12 @@ def test_set_log_prefix_updates_all_handlers():
module.set_log_prefix('myprefix')
for style in styles:
assert style._defaults == {'prefix': 'myprefix: '}
for formatter in formatters:
assert formatter.prefix == 'myprefix'
def test_set_log_prefix_skips_handlers_without_a_formatter():
style = flexmock(_defaults=None)
formatter = flexmock(prefix=None)
flexmock(module.logging).should_receive('getLogger').and_return(
flexmock(
@@ -326,11 +317,8 @@ def test_set_log_prefix_skips_handlers_without_a_formatter():
flexmock(
formatter=None,
),
flexmock(),
flexmock(
formatter=flexmock(
_style=style,
)
formatter=formatter,
),
],
removeHandler=lambda handler: None,
@@ -339,7 +327,7 @@ def test_set_log_prefix_skips_handlers_without_a_formatter():
module.set_log_prefix('myprefix')
assert style._defaults == {'prefix': 'myprefix: '}
assert formatter.prefix == 'myprefix'
def test_log_prefix_sets_prefix_and_then_restores_no_prefix_after():
@@ -360,6 +348,94 @@ def test_log_prefix_sets_prefix_and_then_restores_original_prefix_after():
pass
def test_delayed_logging_handler_should_flush_without_targets_returns_false():
handler = module.Delayed_logging_handler()
assert handler.shouldFlush(flexmock()) is False
def test_delayed_logging_handler_should_flush_with_targets_returns_true():
handler = module.Delayed_logging_handler()
handler.targets = [flexmock()]
assert handler.shouldFlush(flexmock()) is True
def test_delayed_logging_handler_flush_without_targets_does_not_raise():
handler = module.Delayed_logging_handler()
flexmock(handler).should_receive('acquire')
flexmock(handler).should_receive('release')
handler.flush()
def test_delayed_logging_handler_flush_with_empty_buffer_does_not_raise():
handler = module.Delayed_logging_handler()
flexmock(handler).should_receive('acquire')
flexmock(handler).should_receive('release')
handler.targets = [flexmock()]
handler.flush()
def test_delayed_logging_handler_flush_forwards_each_record_to_each_target():
handler = module.Delayed_logging_handler()
flexmock(handler).should_receive('acquire')
flexmock(handler).should_receive('release')
handler.targets = [flexmock(level=logging.DEBUG), flexmock(level=logging.DEBUG)]
handler.buffer = [flexmock(levelno=logging.DEBUG), flexmock(levelno=logging.DEBUG)]
handler.targets[0].should_receive('handle').with_args(handler.buffer[0]).once()
handler.targets[1].should_receive('handle').with_args(handler.buffer[0]).once()
handler.targets[0].should_receive('handle').with_args(handler.buffer[1]).once()
handler.targets[1].should_receive('handle').with_args(handler.buffer[1]).once()
handler.flush()
assert handler.buffer == []
def test_delayed_logging_handler_flush_skips_forwarding_when_log_record_is_too_low_for_target():
handler = module.Delayed_logging_handler()
flexmock(handler).should_receive('acquire')
flexmock(handler).should_receive('release')
handler.targets = [flexmock(level=logging.INFO), flexmock(level=logging.DEBUG)]
handler.buffer = [flexmock(levelno=logging.DEBUG), flexmock(levelno=logging.INFO)]
handler.targets[0].should_receive('handle').with_args(handler.buffer[0]).never()
handler.targets[1].should_receive('handle').with_args(handler.buffer[0]).once()
handler.targets[0].should_receive('handle').with_args(handler.buffer[1]).once()
handler.targets[1].should_receive('handle').with_args(handler.buffer[1]).once()
handler.flush()
assert handler.buffer == []
def test_flush_delayed_logging_without_handlers_does_not_raise():
root_logger = flexmock(handlers=[])
root_logger.should_receive('removeHandler')
flexmock(module.logging).should_receive('getLogger').and_return(root_logger)
module.flush_delayed_logging([flexmock()])
def test_flush_delayed_logging_without_delayed_logging_handler_does_not_raise():
root_logger = flexmock(handlers=[flexmock()])
root_logger.should_receive('removeHandler')
flexmock(module.logging).should_receive('getLogger').and_return(root_logger)
module.flush_delayed_logging([flexmock()])
def test_flush_delayed_logging_flushes_delayed_logging_handler():
delayed_logging_handler = module.Delayed_logging_handler()
root_logger = flexmock(handlers=[delayed_logging_handler])
flexmock(module.logging).should_receive('getLogger').and_return(root_logger)
flexmock(delayed_logging_handler).should_receive('flush').once()
root_logger.should_receive('removeHandler')
module.flush_delayed_logging([flexmock()])
def test_configure_logging_with_syslog_log_level_probes_for_log_socket_on_linux():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.ANSWER
@@ -369,6 +445,7 @@ def test_configure_logging_with_syslog_log_level_probes_for_log_socket_on_linux(
multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
flexmock(module).should_receive('interactive_console').and_return(False)
flexmock(module).should_receive('flush_delayed_logging')
flexmock(module.logging).should_receive('basicConfig').with_args(
level=logging.DEBUG, handlers=list
)
@@ -390,6 +467,7 @@ def test_configure_logging_with_syslog_log_level_probes_for_log_socket_on_macos(
multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
flexmock(module).should_receive('interactive_console').and_return(False)
flexmock(module).should_receive('flush_delayed_logging')
flexmock(module.logging).should_receive('basicConfig').with_args(
level=logging.DEBUG, handlers=list
)
@@ -412,6 +490,7 @@ def test_configure_logging_with_syslog_log_level_probes_for_log_socket_on_freebs
multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
flexmock(module).should_receive('interactive_console').and_return(False)
flexmock(module).should_receive('flush_delayed_logging')
flexmock(module.logging).should_receive('basicConfig').with_args(
level=logging.DEBUG, handlers=list
)
@@ -434,6 +513,7 @@ def test_configure_logging_without_syslog_log_level_skips_syslog():
multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO)
multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
flexmock(module).should_receive('flush_delayed_logging')
flexmock(module.logging).should_receive('basicConfig').with_args(
level=logging.INFO, handlers=list
)
@@ -451,6 +531,7 @@ def test_configure_logging_skips_syslog_if_not_found():
multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO)
multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
flexmock(module).should_receive('flush_delayed_logging')
flexmock(module.logging).should_receive('basicConfig').with_args(
level=logging.INFO, handlers=list
)
@@ -469,6 +550,7 @@ def test_configure_logging_skips_log_file_if_log_file_logging_is_disabled():
multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
flexmock(module).should_receive('flush_delayed_logging')
flexmock(module.logging).should_receive('basicConfig').with_args(
level=logging.INFO, handlers=list
)
@@ -490,6 +572,7 @@ def test_configure_logging_to_log_file_instead_of_syslog():
multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
flexmock(module).should_receive('flush_delayed_logging')
flexmock(module.logging).should_receive('basicConfig').with_args(
level=logging.DEBUG, handlers=list
)
@@ -517,6 +600,7 @@ def test_configure_logging_to_both_log_file_and_syslog():
multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
flexmock(module).should_receive('flush_delayed_logging')
flexmock(module.logging).should_receive('basicConfig').with_args(
level=logging.DEBUG, handlers=list
)
@@ -541,10 +625,8 @@ def test_configure_logging_to_both_log_file_and_syslog():
def test_configure_logging_to_log_file_formats_with_custom_log_format():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.ANSWER
flexmock(module.logging).should_receive('Formatter').with_args(
flexmock(module).should_receive('Log_prefix_formatter').with_args(
'{message}', # noqa: FS003
style='{',
defaults={'prefix': ''},
).once()
fake_formatter = flexmock()
flexmock(module).should_receive('Console_color_formatter').and_return(fake_formatter)
@@ -553,6 +635,7 @@ def test_configure_logging_to_log_file_formats_with_custom_log_format():
flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
flexmock(module).should_receive('interactive_console').and_return(False)
flexmock(module).should_receive('flush_delayed_logging')
flexmock(module.logging).should_receive('basicConfig').with_args(
level=logging.DEBUG, handlers=list
)
@@ -580,6 +663,7 @@ def test_configure_logging_skips_log_file_if_argument_is_none():
multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
flexmock(module).should_receive('flush_delayed_logging')
flexmock(module.logging).should_receive('basicConfig').with_args(
level=logging.INFO, handlers=list
)
@@ -594,11 +678,12 @@ def test_configure_logging_uses_console_no_color_formatter_if_color_disabled():
flexmock(module.logging).ANSWER = module.ANSWER
fake_formatter = flexmock()
flexmock(module).should_receive('Console_color_formatter').never()
flexmock(module).should_receive('Console_no_color_formatter').and_return(fake_formatter)
flexmock(module).should_receive('Log_prefix_formatter').and_return(fake_formatter)
multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO)
multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
flexmock(module).should_receive('flush_delayed_logging')
flexmock(module.logging).should_receive('basicConfig').with_args(
level=logging.INFO, handlers=list
)
+1 -1
View File
@@ -1,5 +1,5 @@
[tox]
env_list = py39,py310,py311,py312
env_list = py39,py310,py311,py312,py313
skip_missing_interpreters = True
package = editable
min_version = 4.0