Add credential loading from file, KeePassXC, and Docker/Podman secrets.
All checks were successful
build / test (push) Successful in 8m40s
build / docs (push) Successful in 1m31s

Reviewed-on: #994
This commit is contained in:
Dan Helfman 2025-02-15 04:20:11 +00:00
commit 7f22612bf1
41 changed files with 1254 additions and 288 deletions

2
NEWS
View File

@ -1,6 +1,8 @@
1.9.11.dev0
* #996: Fix the "create" action to omit the repository label prefix from Borg's output when
databases are enabled.
* Add credential loading from file, KeePassXC, and Docker/Podman secrets. See the documentation for
more information: https://torsion.org/borgmatic/docs/how-to/provide-your-passwords/
1.9.10
* #966: Add a "{credential ...}" syntax for loading systemd credentials into borgmatic

View File

@ -88,6 +88,9 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
### Credentials
<a href="https://systemd.io/"><img src="docs/static/systemd.png" alt="Sentry" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://www.docker.com/"><img src="docs/static/docker.png" alt="Docker" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://podman.io/"><img src="docs/static/podman.png" alt="Podman" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://keepassxc.org/"><img src="docs/static/keepassxc.png" alt="Podman" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
## Getting started

View File

@ -41,7 +41,7 @@ def make_environment(config):
value = config.get(option_name)
if option_name in CREDENTIAL_OPTIONS and value is not None:
value = borgmatic.hooks.credential.parse.resolve_credential(value)
value = borgmatic.hooks.credential.parse.resolve_credential(value, config)
if value is not None:
environment[environment_variable_name] = str(value)

View File

@ -2402,3 +2402,25 @@ properties:
description: |
Configuration for integration with Linux LVM (Logical Volume
Manager).
container:
type: object
additionalProperties: false
properties:
secrets_directory:
type: string
description: |
Secrets directory to use instead of "/run/secrets".
example: /path/to/secrets
description: |
Configuration for integration with Docker or Podman secrets.
keepassxc:
type: object
additionalProperties: false
properties:
keepassxc_cli_command:
type: string
description: |
Command to use instead of "keepassxc-cli".
example: /usr/local/bin/keepassxc-cli
description: |
Configuration for integration with the KeePassXC password manager.

View File

@ -0,0 +1,41 @@
import logging
import os
import re
logger = logging.getLogger(__name__)
SECRET_NAME_PATTERN = re.compile(r'^\w+$')
DEFAULT_SECRETS_DIRECTORY = '/run/secrets'
def load_credential(hook_config, config, credential_parameters):
'''
Given the hook configuration dict, the configuration dict, and a credential parameters tuple
containing a secret name to load, read the secret from the corresponding container secrets file
and return it.
Raise ValueError if the credential parameters is not one element, the secret name is invalid, or
the secret file cannot be read.
'''
try:
(secret_name,) = credential_parameters
except ValueError:
raise ValueError(f'Cannot load invalid secret name: "{' '.join(credential_parameters)}"')
if not SECRET_NAME_PATTERN.match(secret_name):
raise ValueError(f'Cannot load invalid secret name: "{secret_name}"')
try:
with open(
os.path.join(
config.get('working_directory', ''),
(hook_config or {}).get('secrets_directory', DEFAULT_SECRETS_DIRECTORY),
secret_name,
)
) as secret_file:
return secret_file.read().rstrip(os.linesep)
except (FileNotFoundError, OSError) as error:
logger.warning(error)
raise ValueError(f'Cannot load secret "{secret_name}" from file: {error.filename}')

View File

@ -0,0 +1,28 @@
import logging
import os
logger = logging.getLogger(__name__)
def load_credential(hook_config, config, credential_parameters):
'''
Given the hook configuration dict, the configuration dict, and a credential parameters tuple
containing a credential path to load, load the credential from file and return it.
Raise ValueError if the credential parameters is not one element or the secret file cannot be
read.
'''
try:
(credential_path,) = credential_parameters
except ValueError:
raise ValueError(f'Cannot load invalid credential: "{' '.join(credential_parameters)}"')
try:
with open(
os.path.join(config.get('working_directory', ''), credential_path)
) as credential_file:
return credential_file.read().rstrip(os.linesep)
except (FileNotFoundError, OSError) as error:
logger.warning(error)
raise ValueError(f'Cannot load credential file: {error.filename}')

View File

@ -0,0 +1,40 @@
import logging
import os
import shlex
import borgmatic.execute
logger = logging.getLogger(__name__)
def load_credential(hook_config, config, credential_parameters):
'''
Given the hook configuration dict, the configuration dict, and a credential parameters tuple
containing a KeePassXC database path and an attribute name to load, run keepassxc-cli to fetch
the corresponidng KeePassXC credential and return it.
Raise ValueError if keepassxc-cli can't retrieve the credential.
'''
try:
(database_path, attribute_name) = credential_parameters
except ValueError:
raise ValueError(
f'Cannot load credential with invalid KeePassXC database path and attribute name: "{' '.join(credential_parameters)}"'
)
if not os.path.exists(database_path):
raise ValueError(
f'Cannot load credential because KeePassXC database path does not exist: {database_path}'
)
return borgmatic.execute.execute_command_and_capture_output(
tuple(shlex.split((hook_config or {}).get('keepassxc_cli_command', 'keepassxc-cli')))
+ (
'show',
'--show-protected',
'--attributes',
'Password',
database_path,
attribute_name,
)
).rstrip(os.linesep)

View File

@ -1,42 +1,124 @@
import functools
import re
import shlex
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):
class Hash_adapter:
'''
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.
A Hash_adapter instance wraps an unhashable object and pretends it's hashable. This is intended
for passing to a @functools.cache-decorated function to prevent it from complaining that an
argument is unhashable. It should only be used for arguments that you don't want to actually
impact the cache hashing, because Hash_adapter doesn't actually hash the object's contents.
Cache the value so repeated calls to this function don't need to load the credential repeatedly.
Example usage:
@functools.cache
def func(a, b):
print(a, b.actual_value)
return a
func(5, Hash_adapter({1: 2, 3: 4})) # Calls func(), prints, and returns.
func(5, Hash_adapter({1: 2, 3: 4})) # Hits the cache and just returns the value.
func(5, Hash_adapter({5: 6, 7: 8})) # Also uses cache, since the Hash_adapter is ignored.
In the above function, the "b" value is one that has been wrapped with Hash_adappter, and
therefore "b.actual_value" is necessary to access the original value.
'''
def __init__(self, actual_value):
self.actual_value = actual_value
def __eq__(self, other):
return True
def __hash__(self):
return 0
UNHASHABLE_TYPES = (dict, list, set)
def cache_ignoring_unhashable_arguments(function):
'''
A function decorator that caches calls to the decorated function but ignores any unhashable
arguments when performing cache lookups. This is intended to be a drop-in replacement for
functools.cache.
Example usage:
@cache_ignoring_unhashable_arguments
def func(a, b):
print(a, b)
return a
func(5, {1: 2, 3: 4}) # Calls func(), prints, and returns.
func(5, {1: 2, 3: 4}) # Hits the cache and just returns the value.
func(5, {5: 6, 7: 8}) # Also uses cache, since the unhashable value (the dict) is ignored.
'''
@functools.cache
def cached_function(*args, **kwargs):
return function(
*(arg.actual_value if isinstance(arg, Hash_adapter) else arg for arg in args),
**{
key: value.actual_value if isinstance(value, Hash_adapter) else value
for (key, value) in kwargs.items()
},
)
@functools.wraps(function)
def wrapper_function(*args, **kwargs):
return cached_function(
*(Hash_adapter(arg) if isinstance(arg, UNHASHABLE_TYPES) else arg for arg in args),
**{
key: Hash_adapter(value) if isinstance(value, UNHASHABLE_TYPES) else value
for (key, value) in kwargs.items()
},
)
wrapper_function.cache_clear = cached_function.cache_clear
return wrapper_function
CREDENTIAL_PATTERN = re.compile(r'\{credential( +(?P<hook_and_parameters>.*))?\}')
@cache_ignoring_unhashable_arguments
def resolve_credential(value, config):
'''
Given a configuration value containing a string like "{credential hookname credentialname}" and
a configuration dict, resolve the credential 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 (ignoring the config for purposes of caching), 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,
)
matcher = CREDENTIAL_PATTERN.match(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):
if not matcher:
return value
hook_and_parameters = matcher.group('hook_and_parameters')
if not hook_and_parameters:
raise ValueError(f'Cannot load credential with invalid syntax "{value}"')
return result
(hook_name, *credential_parameters) = shlex.split(hook_and_parameters)
if not credential_parameters:
raise ValueError(f'Cannot load credential with invalid syntax "{value}"')
return borgmatic.hooks.dispatch.call_hook(
'load_credential', config, hook_name, tuple(credential_parameters)
)

View File

@ -8,14 +8,22 @@ logger = logging.getLogger(__name__)
CREDENTIAL_NAME_PATTERN = re.compile(r'^\w+$')
def load_credential(hook_config, config, credential_name):
def load_credential(hook_config, config, credential_parameters):
'''
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.
Given the hook configuration dict, the configuration dict, and a credential parameters tuple
containing 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.
'''
try:
(credential_name,) = credential_parameters
except ValueError:
raise ValueError(
f'Cannot load invalid credential name: "{' '.join(credential_parameters)}"'
)
credentials_directory = os.environ.get('CREDENTIALS_DIRECTORY')
if not credentials_directory:

View File

@ -26,11 +26,11 @@ def make_dump_path(base_directory): # pragma: no cover
SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys')
def database_names_to_dump(database, extra_environment, dry_run):
def database_names_to_dump(database, config, extra_environment, dry_run):
'''
Given a requested database config, return the corresponding sequence of database names to dump.
In the case of "all", query for the names of databases on the configured host and return them,
excluding any system databases that will cause problems during restore.
Given a requested database config and a configuration dict, return the corresponding sequence of
database names to dump. In the case of "all", query for the names of databases on the configured
host and return them, excluding any system databases that will cause problems during restore.
'''
if database['name'] != 'all':
return (database['name'],)
@ -47,7 +47,10 @@ def database_names_to_dump(database, extra_environment, dry_run):
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
+ (
('--user', borgmatic.hooks.credential.parse.resolve_credential(database['username']))
(
'--user',
borgmatic.hooks.credential.parse.resolve_credential(database['username'], config),
)
if 'username' in database
else ()
)
@ -67,7 +70,7 @@ def database_names_to_dump(database, extra_environment, dry_run):
def execute_dump_command(
database, dump_path, database_names, extra_environment, dry_run, dry_run_label
database, config, dump_path, database_names, extra_environment, dry_run, dry_run_label
):
'''
Kick off a dump for the given MariaDB database (provided as a configuration dict) to a named
@ -102,7 +105,10 @@ def execute_dump_command(
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
+ (
('--user', borgmatic.hooks.credential.parse.resolve_credential(database['username']))
(
'--user',
borgmatic.hooks.credential.parse.resolve_credential(database['username'], config),
)
if 'username' in database
else ()
)
@ -162,11 +168,15 @@ def dump_data_sources(
for database in databases:
dump_path = make_dump_path(borgmatic_runtime_directory)
extra_environment = (
{'MYSQL_PWD': borgmatic.hooks.credential.parse.resolve_credential(database['password'])}
{
'MYSQL_PWD': borgmatic.hooks.credential.parse.resolve_credential(
database['password'], config
)
}
if 'password' in database
else None
)
dump_database_names = database_names_to_dump(database, extra_environment, dry_run)
dump_database_names = database_names_to_dump(database, config, extra_environment, dry_run)
if not dump_database_names:
if dry_run:
@ -181,6 +191,7 @@ def dump_data_sources(
processes.append(
execute_dump_command(
renamed_database,
config,
dump_path,
(dump_name,),
extra_environment,
@ -192,6 +203,7 @@ def dump_data_sources(
processes.append(
execute_dump_command(
database,
config,
dump_path,
dump_database_names,
extra_environment,
@ -265,12 +277,18 @@ def restore_data_source_dump(
connection_params['port'] or data_source.get('restore_port', data_source.get('port', ''))
)
username = borgmatic.hooks.credential.parse.resolve_credential(
connection_params['username']
or data_source.get('restore_username', data_source.get('username'))
(
connection_params['username']
or data_source.get('restore_username', data_source.get('username'))
),
config,
)
password = borgmatic.hooks.credential.parse.resolve_credential(
connection_params['password']
or data_source.get('restore_password', data_source.get('password'))
(
connection_params['password']
or data_source.get('restore_password', data_source.get('password'))
),
config,
)
mariadb_restore_command = tuple(

View File

@ -69,7 +69,7 @@ def dump_data_sources(
if dry_run:
continue
command = build_dump_command(database, dump_filename, dump_format)
command = build_dump_command(database, config, dump_filename, dump_format)
if dump_format == 'directory':
dump.create_parent_directory_for_dump(dump_filename)
@ -88,7 +88,7 @@ def dump_data_sources(
return processes
def build_dump_command(database, dump_filename, dump_format):
def build_dump_command(database, config, dump_filename, dump_format):
'''
Return the mongodump command from a single database configuration.
'''
@ -103,7 +103,9 @@ def build_dump_command(database, dump_filename, dump_format):
(
'--username',
shlex.quote(
borgmatic.hooks.credential.parse.resolve_credential(database['username'])
borgmatic.hooks.credential.parse.resolve_credential(
database['username'], config
)
),
)
if 'username' in database
@ -113,7 +115,9 @@ def build_dump_command(database, dump_filename, dump_format):
(
'--password',
shlex.quote(
borgmatic.hooks.credential.parse.resolve_credential(database['password'])
borgmatic.hooks.credential.parse.resolve_credential(
database['password'], config
)
),
)
if 'password' in database
@ -192,7 +196,7 @@ def restore_data_source_dump(
data_source.get('hostname'),
)
restore_command = build_restore_command(
extract_process, data_source, dump_filename, connection_params
extract_process, data_source, config, dump_filename, connection_params
)
logger.debug(f"Restoring MongoDB database {data_source['name']}{dry_run_label}")
@ -209,7 +213,7 @@ def restore_data_source_dump(
)
def build_restore_command(extract_process, database, dump_filename, connection_params):
def build_restore_command(extract_process, database, config, dump_filename, connection_params):
'''
Return the mongorestore command from a single database configuration.
'''
@ -218,10 +222,18 @@ def build_restore_command(extract_process, database, dump_filename, connection_p
)
port = str(connection_params['port'] or database.get('restore_port', database.get('port', '')))
username = borgmatic.hooks.credential.parse.resolve_credential(
connection_params['username'] or database.get('restore_username', database.get('username'))
(
connection_params['username']
or database.get('restore_username', database.get('username'))
),
config,
)
password = borgmatic.hooks.credential.parse.resolve_credential(
connection_params['password'] or database.get('restore_password', database.get('password'))
(
connection_params['password']
or database.get('restore_password', database.get('password'))
),
config,
)
command = ['mongorestore']

View File

@ -26,11 +26,11 @@ def make_dump_path(base_directory): # pragma: no cover
SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys')
def database_names_to_dump(database, extra_environment, dry_run):
def database_names_to_dump(database, config, extra_environment, dry_run):
'''
Given a requested database config, return the corresponding sequence of database names to dump.
In the case of "all", query for the names of databases on the configured host and return them,
excluding any system databases that will cause problems during restore.
Given a requested database config and a configuration dict, return the corresponding sequence of
database names to dump. In the case of "all", query for the names of databases on the configured
host and return them, excluding any system databases that will cause problems during restore.
'''
if database['name'] != 'all':
return (database['name'],)
@ -47,7 +47,10 @@ def database_names_to_dump(database, extra_environment, dry_run):
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
+ (
('--user', borgmatic.hooks.credential.parse.resolve_credential(database['username']))
(
'--user',
borgmatic.hooks.credential.parse.resolve_credential(database['username'], config),
)
if 'username' in database
else ()
)
@ -67,7 +70,7 @@ def database_names_to_dump(database, extra_environment, dry_run):
def execute_dump_command(
database, dump_path, database_names, extra_environment, dry_run, dry_run_label
database, config, dump_path, database_names, extra_environment, dry_run, dry_run_label
):
'''
Kick off a dump for the given MySQL/MariaDB database (provided as a configuration dict) to a
@ -101,7 +104,10 @@ def execute_dump_command(
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
+ (
('--user', borgmatic.hooks.credential.parse.resolve_credential(database['username']))
(
'--user',
borgmatic.hooks.credential.parse.resolve_credential(database['username'], config),
)
if 'username' in database
else ()
)
@ -161,11 +167,15 @@ def dump_data_sources(
for database in databases:
dump_path = make_dump_path(borgmatic_runtime_directory)
extra_environment = (
{'MYSQL_PWD': borgmatic.hooks.credential.parse.resolve_credential(database['password'])}
{
'MYSQL_PWD': borgmatic.hooks.credential.parse.resolve_credential(
database['password'], config
)
}
if 'password' in database
else None
)
dump_database_names = database_names_to_dump(database, extra_environment, dry_run)
dump_database_names = database_names_to_dump(database, config, extra_environment, dry_run)
if not dump_database_names:
if dry_run:
@ -180,6 +190,7 @@ def dump_data_sources(
processes.append(
execute_dump_command(
renamed_database,
config,
dump_path,
(dump_name,),
extra_environment,
@ -191,6 +202,7 @@ def dump_data_sources(
processes.append(
execute_dump_command(
database,
config,
dump_path,
dump_database_names,
extra_environment,
@ -264,12 +276,18 @@ def restore_data_source_dump(
connection_params['port'] or data_source.get('restore_port', data_source.get('port', ''))
)
username = borgmatic.hooks.credential.parse.resolve_credential(
connection_params['username']
or data_source.get('restore_username', data_source.get('username'))
(
connection_params['username']
or data_source.get('restore_username', data_source.get('username'))
),
config,
)
password = borgmatic.hooks.credential.parse.resolve_credential(
connection_params['password']
or data_source.get('restore_password', data_source.get('password'))
(
connection_params['password']
or data_source.get('restore_password', data_source.get('password'))
),
config,
)
mysql_restore_command = tuple(

View File

@ -25,7 +25,7 @@ def make_dump_path(base_directory): # pragma: no cover
return dump.make_data_source_dump_path(base_directory, 'postgresql_databases')
def make_extra_environment(database, restore_connection_params=None):
def make_extra_environment(database, config, restore_connection_params=None):
'''
Make the extra_environment dict from the given database configuration. If restore connection
params are given, this is for a restore operation.
@ -35,12 +35,15 @@ def make_extra_environment(database, restore_connection_params=None):
try:
if restore_connection_params:
extra['PGPASSWORD'] = borgmatic.hooks.credential.parse.resolve_credential(
restore_connection_params.get('password')
or database.get('restore_password', database['password'])
(
restore_connection_params.get('password')
or database.get('restore_password', database['password'])
),
config,
)
else:
extra['PGPASSWORD'] = borgmatic.hooks.credential.parse.resolve_credential(
database['password']
database['password'], config
)
except (AttributeError, KeyError):
pass
@ -62,12 +65,12 @@ def make_extra_environment(database, restore_connection_params=None):
EXCLUDED_DATABASE_NAMES = ('template0', 'template1')
def database_names_to_dump(database, extra_environment, dry_run):
def database_names_to_dump(database, config, extra_environment, dry_run):
'''
Given a requested database config, return the corresponding sequence of database names to dump.
In the case of "all" when a database format is given, query for the names of databases on the
configured host and return them. For "all" without a database format, just return a sequence
containing "all".
Given a requested database config and a configuration dict, return the corresponding sequence of
database names to dump. In the case of "all" when a database format is given, query for the
names of databases on the configured host and return them. For "all" without a database format,
just return a sequence containing "all".
'''
requested_name = database['name']
@ -89,7 +92,7 @@ def database_names_to_dump(database, extra_environment, dry_run):
+ (
(
'--username',
borgmatic.hooks.credential.parse.resolve_credential(database['username']),
borgmatic.hooks.credential.parse.resolve_credential(database['username'], config),
)
if 'username' in database
else ()
@ -146,9 +149,9 @@ def dump_data_sources(
logger.info(f'Dumping PostgreSQL databases{dry_run_label}')
for database in databases:
extra_environment = make_extra_environment(database)
extra_environment = make_extra_environment(database, config)
dump_path = make_dump_path(borgmatic_runtime_directory)
dump_database_names = database_names_to_dump(database, extra_environment, dry_run)
dump_database_names = database_names_to_dump(database, config, extra_environment, dry_run)
if not dump_database_names:
if dry_run:
@ -189,7 +192,7 @@ def dump_data_sources(
'--username',
shlex.quote(
borgmatic.hooks.credential.parse.resolve_credential(
database['username']
database['username'], config
)
),
)
@ -309,8 +312,11 @@ def restore_data_source_dump(
connection_params['port'] or data_source.get('restore_port', data_source.get('port', ''))
)
username = borgmatic.hooks.credential.parse.resolve_credential(
connection_params['username']
or data_source.get('restore_username', data_source.get('username'))
(
connection_params['username']
or data_source.get('restore_username', data_source.get('username'))
),
config,
)
all_databases = bool(data_source['name'] == 'all')
@ -364,7 +370,7 @@ def restore_data_source_dump(
)
extra_environment = make_extra_environment(
data_source, restore_connection_params=connection_params
data_source, config, restore_connection_params=connection_params
)
logger.debug(f"Restoring PostgreSQL database {data_source['name']}{dry_run_label}")

View File

@ -51,13 +51,13 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
try:
username = borgmatic.hooks.credential.parse.resolve_credential(
hook_config.get('username')
hook_config.get('username'), config
)
password = borgmatic.hooks.credential.parse.resolve_credential(
hook_config.get('password')
hook_config.get('password'), config
)
access_token = borgmatic.hooks.credential.parse.resolve_credential(
hook_config.get('access_token')
hook_config.get('access_token'), config
)
except ValueError as error:
logger.warning(f'Ntfy credential error: {error}')

View File

@ -42,7 +42,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
try:
integration_key = borgmatic.hooks.credential.parse.resolve_credential(
hook_config.get('integration_key')
hook_config.get('integration_key'), config
)
except ValueError as error:
logger.warning(f'PagerDuty credential error: {error}')

View File

@ -35,8 +35,10 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
state_config = hook_config.get(state.name.lower(), {})
try:
token = borgmatic.hooks.credential.parse.resolve_credential(hook_config.get('token'))
user = borgmatic.hooks.credential.parse.resolve_credential(hook_config.get('user'))
token = borgmatic.hooks.credential.parse.resolve_credential(
hook_config.get('token'), config
)
user = borgmatic.hooks.credential.parse.resolve_credential(hook_config.get('user'), config)
except ValueError as error:
logger.warning(f'Pushover credential error: {error}')
return

View File

@ -37,9 +37,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'))
username = borgmatic.hooks.credential.parse.resolve_credential(
hook_config.get('username'), config
)
password = borgmatic.hooks.credential.parse.resolve_credential(
hook_config.get('password'), config
)
api_key = borgmatic.hooks.credential.parse.resolve_credential(
hook_config.get('api_key'), config
)
except ValueError as error:
logger.warning(f'Zabbix credential error: {error}')
return

View File

@ -19,6 +19,7 @@ encryption_passphrase: yourpassphrase
But if you'd rather store them outside of borgmatic, whether for convenience
or security reasons, read on.
### Delegating to another application
borgmatic supports calling another application such as a password manager to
@ -31,15 +32,6 @@ to provide the passphrase:
encryption_passcommand: pass path/to/borg-passphrase
```
Another example for [KeePassXC](https://keepassxc.org/):
```yaml
encryption_passcommand: keepassxc-cli show --show-protected --attributes Password credentials.kdbx borg_passphrase
```
... where `borg_passphrase` is the title of the KeePassXC entry containing your
Borg encryption passphrase in its `Password` field.
<span class="minilink minilink-addedin">New in version 1.9.9</span> Instead of
letting Borg run the passcommand—potentially multiple times since borgmatic runs
Borg multiple times—borgmatic now runs the passcommand itself and passes the
@ -48,9 +40,9 @@ should only ever get prompted for your password manager's passphrase at most
once per borgmatic run.
### Using systemd service credentials
### systemd service credentials
borgmatic supports using encrypted [systemd
borgmatic supports reading 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.,
@ -146,13 +138,172 @@ The one exception is `borgmatic config validate`, which doesn't actually load
any credentials and should continue working anywhere.
### Container secrets
<span class="minilink minilink-addedin">New in version 1.9.11</span> When
running inside a container, borgmatic can read [Docker
secrets](https://docs.docker.com/compose/how-tos/use-secrets/) and [Podman
secrets](https://www.redhat.com/en/blog/new-podman-secrets-command). Creating
those secrets and passing them into your borgmatic container is outside the
scope of this documentation, but here's a simple example of that with [Docker
Compose](https://docs.docker.com/compose/):
```yaml
services:
borgmatic:
# Use the actual image name of your borgmatic container here.
image: borgmatic:latest
secrets:
- borgmatic_passphrase
secrets:
borgmatic_passphrase:
file: /etc/borgmatic/passphrase.txt
```
This assumes there's a file on the host at `/etc/borgmatic/passphrase.txt`
containing your passphrase. Docker or Podman mounts the contents of that file
into a secret named `borgmatic_passphrase` in the borgmatic container at
`/run/secrets/`.
Once your container secret is in place, you can consume it within your borgmatic
configuration file:
```yaml
encryption_passphrase: "{credential container borgmatic_passphrase}"
```
This reads the secret securely from a file mounted at
`/run/secrets/borgmatic_passphrase` within the borgmatic container.
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 container borgmatic_db1}"
```
For specifics about which options are supported, see the
[configuration
reference](https://torsion.org/borgmatic/docs/reference/configuration/).
You can also optionally override the `/run/secrets` directory that borgmatic reads secrets from
inside a container:
```yaml
container:
secrets_directory: /path/to/secrets
```
But you should only need to do this for development or testing purposes.
### KeePassXC passwords
<span class="minilink minilink-addedin">New in version 1.9.11</span> borgmatic
supports reading passwords from the [KeePassXC](https://keepassxc.org/) password
manager. To use this feature, start by creating an entry in your KeePassXC
database, putting your password into the "Password" field of that entry and
making sure it's saved.
Then, you can consume that password in your borgmatic configuration file. For
instance, if the entry's title is "borgmatic" and your KeePassXC database is
located at `/etc/keys.kdbx`, do this:
```yaml
encryption_passphrase: "{credential keepassxc /etc/keys.kdbx borgmatic}"
```
But if the entry's title is multiple words like `borg pw`, you'll
need to quote it:
```yaml
encryption_passphrase: "{credential keepassxc /etc/keys.kdbx 'borg pw'}"
```
With this in place, borgmatic runs the `keepassxc-cli` command to retrieve the
passphrase on demand. But note that `keepassxc-cli` will prompt for its own
passphrase in order to unlock its database, so be prepared to enter it when
running borgmatic.
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 keepassxc /etc/keys.kdbx database}"
```
For specifics about which options are supported, see the
[configuration
reference](https://torsion.org/borgmatic/docs/reference/configuration/).
You can also optionally override the `keepassxc-cli` command that borgmatic calls to load
passwords:
```yaml
keepassxc:
keepassxc_cli_command: /usr/local/bin/keepassxc-cli
```
### File-based credentials
<span class="minilink minilink-addedin">New in version 1.9.11</span> borgmatic
supports reading credentials from arbitrary file paths. To use this feature,
start by writing your credential into a file that borgmatic has permission to
read. Take care not to include anything in the file other than your credential.
(borgmatic is smart enough to strip off a trailing newline though.)
You can consume that credential file in your borgmatic configuration. For
instance, if your credential file is at `/credentials/borgmatic.txt`, do this:
```yaml
encryption_passphrase: "{credential file /credentials/borgmatic.txt}"
```
With this in place, borgmatic reads the credential from the file path.
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 file /credentials/database.txt}"
```
For specifics about which options are supported, see the
[configuration
reference](https://torsion.org/borgmatic/docs/reference/configuration/).
### Environment variable interpolation
<span class="minilink minilink-addedin">New in version 1.6.4</span> borgmatic
supports interpolating arbitrary environment variables directly into option
values in your configuration file. That means you can instruct borgmatic to
pull your repository passphrase, your database passwords, or any other option
values from environment variables. For instance:
values from environment variables.
Be aware though that environment variables may be less secure than some of the
other approaches above for getting credentials into borgmatic. That's because
environment variables may be visible from within child processes and/or OS-level
process metadata.
Here's an example of using an environment variable from borgmatic's
configuration file:
```yaml
encryption_passphrase: ${YOUR_PASSPHRASE}
@ -214,6 +365,7 @@ can escape it with a backslash. For instance, if your password is literally
encryption_passphrase: \${A}@!
```
## Related features
Another way to override particular options within a borgmatic configuration
@ -226,9 +378,3 @@ Additionally, borgmatic action hooks support their own [variable
interpolation](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/#variable-interpolation),
although in that case it's for particular borgmatic runtime values rather than
(only) environment variables.
Lastly, if you do want to specify your passhprase directly within borgmatic
configuration, but you'd like to keep it in a separate file from your main
configuration, you can [use a configuration include or a merge
include](https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/#configuration-includes)
to pull in an external password.

BIN
docs/static/docker.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
docs/static/keepassxc.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
docs/static/podman.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@ -0,0 +1,29 @@
import argparse
import sys
def parse_arguments(*unparsed_arguments):
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('command')
parser.add_argument('--show-protected', action='store_true')
parser.add_argument('--attributes')
parser.add_argument('database_path')
parser.add_argument('attribute_name')
return parser.parse_args(unparsed_arguments)
def main():
arguments = parse_arguments(*sys.argv[1:])
assert arguments.command == 'show'
assert arguments.show_protected
assert arguments.attributes == 'Password'
assert arguments.database_path.endswith('.kdbx')
assert arguments.attribute_name
print('test')
if __name__ == '__main__':
main()

View File

@ -0,0 +1,68 @@
import json
import os
import shutil
import subprocess
import sys
import tempfile
def generate_configuration(config_path, repository_path, secrets_directory):
'''
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 container secrets in the given secrets
directory.
'''
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 container mysecret}"'
+ f'\ncontainer:\n secrets_directory: {secrets_directory}'
)
config_file = open(config_path, 'w')
config_file.write(config)
config_file.close()
def test_container_secret():
# Create a Borg repository.
temporary_directory = tempfile.mkdtemp()
repository_path = os.path.join(temporary_directory, 'test.borg')
original_working_directory = os.getcwd()
os.chdir(temporary_directory)
try:
config_path = os.path.join(temporary_directory, 'test.yaml')
generate_configuration(config_path, repository_path, secrets_directory=temporary_directory)
secret_path = os.path.join(temporary_directory, 'mysecret')
with open(secret_path, 'w') as secret_file:
secret_file.write('test')
subprocess.check_call(
f'borgmatic -v 2 --config {config_path} repo-create --encryption repokey'.split(' '),
)
# 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(' '),
)
output = subprocess.check_output(
f'borgmatic --config {config_path} list --json'.split(' '),
).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)

View File

@ -0,0 +1,68 @@
import json
import os
import shutil
import subprocess
import sys
import tempfile
def generate_configuration(config_path, repository_path, credential_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 file at the given credential path.
'''
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 file '
+ credential_path
+ '}"'
)
config_file = open(config_path, 'w')
config_file.write(config)
config_file.close()
def test_file_credential():
# Create a Borg repository.
temporary_directory = tempfile.mkdtemp()
repository_path = os.path.join(temporary_directory, 'test.borg')
original_working_directory = os.getcwd()
os.chdir(temporary_directory)
try:
config_path = os.path.join(temporary_directory, 'test.yaml')
credential_path = os.path.join(temporary_directory, 'mycredential')
generate_configuration(config_path, repository_path, credential_path)
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(' '),
)
# 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(' '),
)
output = subprocess.check_output(
f'borgmatic --config {config_path} list --json'.split(' '),
).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)

View File

@ -0,0 +1,67 @@
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 keepassxc-cli.
'''
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 keepassxc keys.kdbx mypassword}"'
+ '\nkeepassxc:\n keepassxc_cli_command: python3 /app/tests/end-to-end/commands/fake_keepassxc_cli.py'
)
config_file = open(config_path, 'w')
config_file.write(config)
config_file.close()
def test_keepassxc_password():
# Create a Borg repository.
temporary_directory = tempfile.mkdtemp()
repository_path = os.path.join(temporary_directory, 'test.borg')
original_working_directory = os.getcwd()
os.chdir(temporary_directory)
try:
config_path = os.path.join(temporary_directory, 'test.yaml')
generate_configuration(config_path, repository_path)
database_path = os.path.join(temporary_directory, 'keys.kdbx')
with open(database_path, 'w') as database_file:
database_file.write('fake KeePassXC database to pacify file existence check')
subprocess.check_call(
f'borgmatic -v 2 --config {config_path} repo-create --encryption repokey'.split(' '),
)
# 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(' '),
)
output = subprocess.check_output(
f'borgmatic --config {config_path} list --json'.split(' '),
).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)

View File

@ -30,15 +30,13 @@ def generate_configuration(config_path, repository_path):
config_file.close()
def test_borgmatic_command():
def test_systemd_credential():
# 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)
os.chdir(temporary_directory)
try:
config_path = os.path.join(temporary_directory, 'test.yaml')

View File

@ -26,7 +26,7 @@ def test_make_environment_with_passphrase_should_set_environment():
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)
).replace_with(lambda value, config: value)
environment = module.make_environment({'encryption_passphrase': 'pass'})
@ -34,16 +34,17 @@ def test_make_environment_with_passphrase_should_set_environment():
def test_make_environment_with_credential_tag_passphrase_should_load_it_and_set_environment():
config = {'encryption_passphrase': '{credential systemd pass}'}
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')
'resolve_credential',
).with_args('{credential systemd pass}', config).and_return('pass')
environment = module.make_environment({'encryption_passphrase': '{credential systemd pass}'})
environment = module.make_environment(config)
assert environment.get('BORG_PASSPHRASE') == 'pass'

View File

@ -0,0 +1,74 @@
import io
import sys
import pytest
from flexmock import flexmock
from borgmatic.hooks.credential import container as module
@pytest.mark.parametrize('credential_parameters', ((), ('foo', 'bar')))
def test_load_credential_with_invalid_credential_parameters_raises(credential_parameters):
with pytest.raises(ValueError):
module.load_credential(
hook_config={}, config={}, credential_parameters=credential_parameters
)
def test_load_credential_with_invalid_secret_name_raises():
with pytest.raises(ValueError):
module.load_credential(
hook_config={}, config={}, credential_parameters=('this is invalid',)
)
def test_load_credential_reads_named_secret_from_file():
credential_stream = io.StringIO('password')
credential_stream.name = '/run/secrets/mysecret'
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('/run/secrets/mysecret').and_return(credential_stream)
assert (
module.load_credential(hook_config={}, config={}, credential_parameters=('mysecret',))
== 'password'
)
def test_load_credential_with_custom_secrets_directory_looks_there_for_secret_file():
config = {'container': {'secrets_directory': '/secrets'}}
credential_stream = io.StringIO('password')
credential_stream.name = '/secrets/mysecret'
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('/secrets/mysecret').and_return(credential_stream)
assert (
module.load_credential(
hook_config=config['container'], config=config, credential_parameters=('mysecret',)
)
== 'password'
)
def test_load_credential_with_custom_secrets_directory_prefixes_it_with_working_directory():
config = {'container': {'secrets_directory': 'secrets'}, 'working_directory': '/working'}
credential_stream = io.StringIO('password')
credential_stream.name = '/working/secrets/mysecret'
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('/working/secrets/mysecret').and_return(
credential_stream
)
assert (
module.load_credential(
hook_config=config['container'], config=config, credential_parameters=('mysecret',)
)
== 'password'
)
def test_load_credential_with_file_not_found_error_raises():
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('/run/secrets/mysecret').and_raise(FileNotFoundError)
with pytest.raises(ValueError):
module.load_credential(hook_config={}, config={}, credential_parameters=('mysecret',))

View File

@ -0,0 +1,68 @@
import io
import sys
import pytest
from flexmock import flexmock
from borgmatic.hooks.credential import file as module
@pytest.mark.parametrize('credential_parameters', ((), ('foo', 'bar')))
def test_load_credential_with_invalid_credential_parameters_raises(credential_parameters):
with pytest.raises(ValueError):
module.load_credential(
hook_config={}, config={}, credential_parameters=credential_parameters
)
def test_load_credential_with_invalid_credential_name_raises():
with pytest.raises(ValueError):
module.load_credential(
hook_config={}, config={}, credential_parameters=('this is invalid',)
)
def test_load_credential_reads_named_credential_from_file():
credential_stream = io.StringIO('password')
credential_stream.name = '/credentials/mycredential'
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('/credentials/mycredential').and_return(
credential_stream
)
assert (
module.load_credential(
hook_config={}, config={}, credential_parameters=('/credentials/mycredential',)
)
== 'password'
)
def test_load_credential_reads_named_credential_from_file_using_working_directory():
credential_stream = io.StringIO('password')
credential_stream.name = '/working/credentials/mycredential'
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('/working/credentials/mycredential').and_return(
credential_stream
)
assert (
module.load_credential(
hook_config={},
config={'working_directory': '/working'},
credential_parameters=('credentials/mycredential',),
)
== 'password'
)
def test_load_credential_with_file_not_found_error_raises():
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('/credentials/mycredential').and_raise(
FileNotFoundError
)
with pytest.raises(ValueError):
module.load_credential(
hook_config={}, config={}, credential_parameters=('/credentials/mycredential',)
)

View File

@ -0,0 +1,80 @@
import pytest
from flexmock import flexmock
from borgmatic.hooks.credential import keepassxc as module
@pytest.mark.parametrize('credential_parameters', ((), ('foo',), ('foo', 'bar', 'baz')))
def test_load_credential_with_invalid_credential_parameters_raises(credential_parameters):
flexmock(module.borgmatic.execute).should_receive('execute_command_and_capture_output').never()
with pytest.raises(ValueError):
module.load_credential(
hook_config={}, config={}, credential_parameters=credential_parameters
)
def test_load_credential_with_missing_database_raises():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.execute).should_receive('execute_command_and_capture_output').never()
with pytest.raises(ValueError):
module.load_credential(
hook_config={}, config={}, credential_parameters=('database.kdbx', 'mypassword')
)
def test_load_credential_with_present_database_fetches_password_from_keepassxc():
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).with_args(
(
'keepassxc-cli',
'show',
'--show-protected',
'--attributes',
'Password',
'database.kdbx',
'mypassword',
)
).and_return(
'password'
).once()
assert (
module.load_credential(
hook_config={}, config={}, credential_parameters=('database.kdbx', 'mypassword')
)
== 'password'
)
def test_load_credential_with_custom_keepassxc_cli_command_calls_it():
config = {'keepassxc': {'keepassxc_cli_command': '/usr/local/bin/keepassxc-cli --some-option'}}
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).with_args(
(
'/usr/local/bin/keepassxc-cli',
'--some-option',
'show',
'--show-protected',
'--attributes',
'Password',
'database.kdbx',
'mypassword',
)
).and_return(
'password'
).once()
assert (
module.load_credential(
hook_config=config['keepassxc'],
config=config,
credential_parameters=('database.kdbx', 'mypassword'),
)
== 'password'
)

View File

@ -4,39 +4,82 @@ from flexmock import flexmock
from borgmatic.hooks.credential import parse as module
def test_resolve_credential_passes_through_string_without_credential_tag():
def test_hash_adapter_is_always_equal():
assert module.Hash_adapter({1: 2}) == module.Hash_adapter({3: 4})
def test_hash_adapter_alwaysh_hashes_the_same():
assert hash(module.Hash_adapter({1: 2})) == hash(module.Hash_adapter({3: 4}))
def test_cache_ignoring_unhashable_arguments_caches_arguments_after_first_call():
hashable = 3
unhashable = {1, 2}
calls = 0
@module.cache_ignoring_unhashable_arguments
def function(first, second, third):
nonlocal calls
calls += 1
assert first == hashable
assert second == unhashable
assert third == unhashable
return first
assert function(hashable, unhashable, third=unhashable) == hashable
assert calls == 1
assert function(hashable, unhashable, third=unhashable) == hashable
assert calls == 1
def test_resolve_credential_passes_through_string_without_credential():
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}'
assert module.resolve_credential('{no credentials here}', config={}) == '{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
assert module.resolve_credential(None, config={}) is None
@pytest.mark.parametrize('invalid_value', ('{credential}', '{credential }', '{credential systemd}'))
def test_resolve_credential_with_invalid_credential_tag_raises(invalid_value):
def test_resolve_credential_with_invalid_credential_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)
module.resolve_credential(invalid_value, config={})
def test_resolve_credential_with_valid_credential_tag_loads_credential():
def test_resolve_credential_with_valid_credential_loads_credential():
module.resolve_credential.cache_clear()
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').with_args(
'load_credential',
{},
'systemd',
'mycredential',
('mycredential',),
).and_return('result').once()
assert module.resolve_credential('{credential systemd mycredential}') == 'result'
assert module.resolve_credential('{credential systemd mycredential}', config={}) == 'result'
def test_resolve_credential_with_valid_credential_and_quoted_parameters_loads_credential():
module.resolve_credential.cache_clear()
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').with_args(
'load_credential',
{},
'systemd',
('my credential',),
).and_return('result').once()
assert module.resolve_credential('{credential systemd "my credential"}', config={}) == 'result'
def test_resolve_credential_caches_credential_after_first_call():
@ -45,8 +88,8 @@ def test_resolve_credential_caches_credential_after_first_call():
'load_credential',
{},
'systemd',
'mycredential',
('mycredential',),
).and_return('result').once()
assert module.resolve_credential('{credential systemd mycredential}') == 'result'
assert module.resolve_credential('{credential systemd mycredential}') == 'result'
assert module.resolve_credential('{credential systemd mycredential}', config={}) == 'result'
assert module.resolve_credential('{credential systemd mycredential}', config={}) == 'result'

View File

@ -7,13 +7,23 @@ from flexmock import flexmock
from borgmatic.hooks.credential import systemd as module
@pytest.mark.parametrize('credential_parameters', ((), ('foo', 'bar')))
def test_load_credential_with_invalid_credential_parameters_raises(credential_parameters):
flexmock(module.os.environ).should_receive('get').never()
with pytest.raises(ValueError):
module.load_credential(
hook_config={}, config={}, credential_parameters=credential_parameters
)
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')
module.load_credential(hook_config={}, config={}, credential_parameters=('mycredential',))
def test_load_credential_with_invalid_credential_name_raises():
@ -22,7 +32,9 @@ def test_load_credential_with_invalid_credential_name_raises():
)
with pytest.raises(ValueError):
module.load_credential(hook_config={}, config={}, credential_name='../../my!@#$credential')
module.load_credential(
hook_config={}, config={}, credential_parameters=('../../my!@#$credential',)
)
def test_load_credential_reads_named_credential_from_file():
@ -35,7 +47,7 @@ def test_load_credential_reads_named_credential_from_file():
builtins.should_receive('open').with_args('/var/mycredential').and_return(credential_stream)
assert (
module.load_credential(hook_config={}, config={}, credential_name='mycredential')
module.load_credential(hook_config={}, config={}, credential_parameters=('mycredential',))
== 'password'
)
@ -48,4 +60,4 @@ def test_load_credential_with_file_not_found_error_raises():
builtins.should_receive('open').with_args('/var/mycredential').and_raise(FileNotFoundError)
with pytest.raises(ValueError):
module.load_credential(hook_config={}, config={}, credential_name='mycredential')
module.load_credential(hook_config={}, config={}, credential_parameters=('mycredential',))

View File

@ -9,7 +9,7 @@ from borgmatic.hooks.data_source import mariadb as module
def test_database_names_to_dump_passes_through_name():
extra_environment = flexmock()
names = module.database_names_to_dump({'name': 'foo'}, extra_environment, dry_run=False)
names = module.database_names_to_dump({'name': 'foo'}, {}, extra_environment, dry_run=False)
assert names == ('foo',)
@ -18,7 +18,7 @@ def test_database_names_to_dump_bails_for_dry_run():
extra_environment = flexmock()
flexmock(module).should_receive('execute_command_and_capture_output').never()
names = module.database_names_to_dump({'name': 'all'}, extra_environment, dry_run=True)
names = module.database_names_to_dump({'name': 'all'}, {}, extra_environment, dry_run=True)
assert names == ()
@ -27,13 +27,13 @@ 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)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('mariadb', '--skip-column-names', '--batch', '--execute', 'show schemas'),
extra_environment=extra_environment,
).and_return('foo\nbar\nmysql\n').once()
names = module.database_names_to_dump({'name': 'all'}, extra_environment, dry_run=False)
names = module.database_names_to_dump({'name': 'all'}, {}, extra_environment, dry_run=False)
assert names == ('foo', 'bar')
@ -55,7 +55,7 @@ def test_dump_data_sources_dumps_each_database():
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
('bar',)
)
@ -63,6 +63,7 @@ def test_dump_data_sources_dumps_each_database():
for name, process in zip(('foo', 'bar'), processes):
flexmock(module).should_receive('execute_dump_command').with_args(
database={'name': name},
config={},
dump_path=object,
database_names=(name,),
extra_environment=object,
@ -89,13 +90,14 @@ def test_dump_data_sources_dumps_with_password():
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
('bar',)
)
flexmock(module).should_receive('execute_dump_command').with_args(
database=database,
config={},
dump_path=object,
database_names=('foo',),
extra_environment={'MYSQL_PWD': 'trustsome1'},
@ -119,10 +121,11 @@ def test_dump_data_sources_dumps_all_databases_at_once():
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: 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'},
config={},
dump_path=object,
database_names=('foo', 'bar'),
extra_environment=object,
@ -146,12 +149,13 @@ def test_dump_data_sources_dumps_all_databases_separately_when_format_configured
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('database_names_to_dump').and_return(('foo', 'bar'))
for name, process in zip(('foo', 'bar'), processes):
flexmock(module).should_receive('execute_dump_command').with_args(
database={'name': name, 'format': 'sql'},
config={},
dump_path=object,
database_names=(name,),
extra_environment=object,
@ -186,7 +190,7 @@ def test_database_names_to_dump_runs_mariadb_with_list_options():
extra_environment=None,
).and_return(('foo\nbar')).once()
assert module.database_names_to_dump(database, None, '') == ('foo', 'bar')
assert module.database_names_to_dump(database, {}, None, '') == ('foo', 'bar')
def test_database_names_to_dump_runs_non_default_mariadb_with_list_options():
@ -207,7 +211,7 @@ def test_database_names_to_dump_runs_non_default_mariadb_with_list_options():
),
).and_return(('foo\nbar')).once()
assert module.database_names_to_dump(database, None, '') == ('foo', 'bar')
assert module.database_names_to_dump(database, {}, None, '') == ('foo', 'bar')
def test_execute_dump_command_runs_mariadb_dump():
@ -216,7 +220,7 @@ def test_execute_dump_command_runs_mariadb_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)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@ -235,6 +239,7 @@ def test_execute_dump_command_runs_mariadb_dump():
assert (
module.execute_dump_command(
database={'name': 'foo'},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
@ -251,7 +256,7 @@ def test_execute_dump_command_runs_mariadb_dump_without_add_drop_database():
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)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@ -269,6 +274,7 @@ def test_execute_dump_command_runs_mariadb_dump_without_add_drop_database():
assert (
module.execute_dump_command(
database={'name': 'foo', 'add_drop_database': False},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
@ -285,7 +291,7 @@ def test_execute_dump_command_runs_mariadb_dump_with_hostname_and_port():
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)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@ -310,6 +316,7 @@ def test_execute_dump_command_runs_mariadb_dump_with_hostname_and_port():
assert (
module.execute_dump_command(
database={'name': 'foo', 'hostname': 'database.example.org', 'port': 5433},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
@ -326,7 +333,7 @@ def test_execute_dump_command_runs_mariadb_dump_with_username_and_password():
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)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@ -347,6 +354,7 @@ def test_execute_dump_command_runs_mariadb_dump_with_username_and_password():
assert (
module.execute_dump_command(
database={'name': 'foo', 'username': 'root', 'password': 'trustsome1'},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment={'MYSQL_PWD': 'trustsome1'},
@ -363,7 +371,7 @@ def test_execute_dump_command_runs_mariadb_dump_with_options():
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)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@ -383,6 +391,7 @@ def test_execute_dump_command_runs_mariadb_dump_with_options():
assert (
module.execute_dump_command(
database={'name': 'foo', 'options': '--stuff=such'},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
@ -399,7 +408,7 @@ def test_execute_dump_command_runs_non_default_mariadb_dump_with_options():
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)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@ -423,6 +432,7 @@ def test_execute_dump_command_runs_non_default_mariadb_dump_with_options():
'mariadb_dump_command': 'custom_mariadb_dump',
'options': '--stuff=such',
}, # Custom MariaDB dump command specified
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
@ -442,6 +452,7 @@ def test_execute_dump_command_with_duplicate_dump_skips_mariadb_dump():
assert (
module.execute_dump_command(
database={'name': 'foo'},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
@ -457,7 +468,7 @@ def test_execute_dump_command_with_dry_run_skips_mariadb_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)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').never()
@ -465,6 +476,7 @@ def test_execute_dump_command_with_dry_run_skips_mariadb_dump():
assert (
module.execute_dump_command(
database={'name': 'foo'},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
@ -480,7 +492,7 @@ def test_dump_data_sources_errors_for_missing_all_databases():
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
'databases/localhost/all'
)
@ -502,7 +514,7 @@ def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
'databases/localhost/all'
)
@ -527,7 +539,7 @@ def test_restore_data_source_dump_runs_mariadb_to_restore():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
('mariadb', '--batch'),
processes=[extract_process],
@ -558,7 +570,7 @@ def test_restore_data_source_dump_runs_mariadb_with_options():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
('mariadb', '--batch', '--harder'),
processes=[extract_process],
@ -591,7 +603,7 @@ def test_restore_data_source_dump_runs_non_default_mariadb_with_options():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
('custom_mariadb', '--batch', '--harder'),
processes=[extract_process],
@ -622,7 +634,7 @@ def test_restore_data_source_dump_runs_mariadb_with_hostname_and_port():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mariadb',
@ -662,7 +674,7 @@ def test_restore_data_source_dump_runs_mariadb_with_username_and_password():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
('mariadb', '--batch', '--user', 'root'),
processes=[extract_process],
@ -703,7 +715,7 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mariadb',
@ -757,7 +769,7 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mariadb',
@ -798,7 +810,7 @@ def test_restore_data_source_dump_with_dry_run_skips_restore():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').never()
module.restore_data_source_dump(

View File

@ -126,7 +126,7 @@ def test_dump_data_sources_runs_mongodump_with_username_and_password():
)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@ -247,9 +247,9 @@ 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)
).replace_with(lambda value, config: value)
command = module.build_dump_command(database, dump_filename='test', dump_format='archive')
command = module.build_dump_command(database, {}, dump_filename='test', dump_format='archive')
assert "'bob; naughty-command'" in command
@ -262,7 +262,7 @@ def test_restore_data_source_dump_runs_mongorestore():
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)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
['mongorestore', '--archive', '--drop'],
processes=[extract_process],
@ -296,7 +296,7 @@ def test_restore_data_source_dump_runs_mongorestore_with_hostname_and_port():
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)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
[
'mongorestore',
@ -344,7 +344,7 @@ def test_restore_data_source_dump_runs_mongorestore_with_username_and_password()
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)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
[
'mongorestore',
@ -398,7 +398,7 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
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)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
[
'mongorestore',
@ -456,7 +456,7 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
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)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
[
'mongorestore',
@ -502,7 +502,7 @@ def test_restore_data_source_dump_runs_mongorestore_with_options():
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)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
['mongorestore', '--archive', '--drop', '--harder'],
processes=[extract_process],
@ -534,7 +534,7 @@ def test_restore_databases_dump_runs_mongorestore_with_schemas():
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)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
[
'mongorestore',
@ -574,7 +574,7 @@ def test_restore_data_source_dump_runs_psql_for_all_database_dump():
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)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
['mongorestore', '--archive'],
processes=[extract_process],
@ -605,7 +605,7 @@ def test_restore_data_source_dump_with_dry_run_skips_restore():
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)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').never()
module.restore_data_source_dump(
@ -631,7 +631,7 @@ def test_restore_data_source_dump_without_extract_process_restores_from_disk():
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)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
['mongorestore', '--dir', '/dump/path', '--drop'],
processes=[],

View File

@ -9,7 +9,7 @@ from borgmatic.hooks.data_source import mysql as module
def test_database_names_to_dump_passes_through_name():
extra_environment = flexmock()
names = module.database_names_to_dump({'name': 'foo'}, extra_environment, dry_run=False)
names = module.database_names_to_dump({'name': 'foo'}, {}, extra_environment, dry_run=False)
assert names == ('foo',)
@ -18,10 +18,10 @@ 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)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_and_capture_output').never()
names = module.database_names_to_dump({'name': 'all'}, extra_environment, dry_run=True)
names = module.database_names_to_dump({'name': 'all'}, {}, extra_environment, dry_run=True)
assert names == ()
@ -30,13 +30,13 @@ 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)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('mysql', '--skip-column-names', '--batch', '--execute', 'show schemas'),
extra_environment=extra_environment,
).and_return('foo\nbar\nmysql\n').once()
names = module.database_names_to_dump({'name': 'all'}, extra_environment, dry_run=False)
names = module.database_names_to_dump({'name': 'all'}, {}, extra_environment, dry_run=False)
assert names == ('foo', 'bar')
@ -63,6 +63,7 @@ def test_dump_data_sources_dumps_each_database():
for name, process in zip(('foo', 'bar'), processes):
flexmock(module).should_receive('execute_dump_command').with_args(
database={'name': name},
config={},
dump_path=object,
database_names=(name,),
extra_environment=object,
@ -89,13 +90,14 @@ def test_dump_data_sources_dumps_with_password():
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
('bar',)
)
flexmock(module).should_receive('execute_dump_command').with_args(
database=database,
config={},
dump_path=object,
database_names=('foo',),
extra_environment={'MYSQL_PWD': 'trustsome1'},
@ -120,6 +122,7 @@ def test_dump_data_sources_dumps_all_databases_at_once():
flexmock(module).should_receive('database_names_to_dump').and_return(('foo', 'bar'))
flexmock(module).should_receive('execute_dump_command').with_args(
database={'name': 'all'},
config={},
dump_path=object,
database_names=('foo', 'bar'),
extra_environment=object,
@ -146,6 +149,7 @@ def test_dump_data_sources_dumps_all_databases_separately_when_format_configured
for name, process in zip(('foo', 'bar'), processes):
flexmock(module).should_receive('execute_dump_command').with_args(
database={'name': name, 'format': 'sql'},
config={},
dump_path=object,
database_names=(name,),
extra_environment=object,
@ -180,7 +184,7 @@ def test_database_names_to_dump_runs_mysql_with_list_options():
extra_environment=None,
).and_return(('foo\nbar')).once()
assert module.database_names_to_dump(database, None, '') == ('foo', 'bar')
assert module.database_names_to_dump(database, {}, None, '') == ('foo', 'bar')
def test_database_names_to_dump_runs_non_default_mysql_with_list_options():
@ -201,7 +205,7 @@ def test_database_names_to_dump_runs_non_default_mysql_with_list_options():
),
).and_return(('foo\nbar')).once()
assert module.database_names_to_dump(database, None, '') == ('foo', 'bar')
assert module.database_names_to_dump(database, {}, None, '') == ('foo', 'bar')
def test_execute_dump_command_runs_mysqldump():
@ -210,7 +214,7 @@ def test_execute_dump_command_runs_mysqldump():
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)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@ -229,6 +233,7 @@ def test_execute_dump_command_runs_mysqldump():
assert (
module.execute_dump_command(
database={'name': 'foo'},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
@ -245,7 +250,7 @@ def test_execute_dump_command_runs_mysqldump_without_add_drop_database():
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)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@ -263,6 +268,7 @@ def test_execute_dump_command_runs_mysqldump_without_add_drop_database():
assert (
module.execute_dump_command(
database={'name': 'foo', 'add_drop_database': False},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
@ -279,7 +285,7 @@ def test_execute_dump_command_runs_mysqldump_with_hostname_and_port():
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)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@ -304,6 +310,7 @@ def test_execute_dump_command_runs_mysqldump_with_hostname_and_port():
assert (
module.execute_dump_command(
database={'name': 'foo', 'hostname': 'database.example.org', 'port': 5433},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
@ -320,7 +327,7 @@ def test_execute_dump_command_runs_mysqldump_with_username_and_password():
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)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@ -341,6 +348,7 @@ def test_execute_dump_command_runs_mysqldump_with_username_and_password():
assert (
module.execute_dump_command(
database={'name': 'foo', 'username': 'root', 'password': 'trustsome1'},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment={'MYSQL_PWD': 'trustsome1'},
@ -357,7 +365,7 @@ def test_execute_dump_command_runs_mysqldump_with_options():
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)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@ -377,6 +385,7 @@ def test_execute_dump_command_runs_mysqldump_with_options():
assert (
module.execute_dump_command(
database={'name': 'foo', 'options': '--stuff=such'},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
@ -393,7 +402,7 @@ def test_execute_dump_command_runs_non_default_mysqldump():
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)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@ -415,6 +424,7 @@ def test_execute_dump_command_runs_non_default_mysqldump():
'name': 'foo',
'mysql_dump_command': 'custom_mysqldump',
}, # Custom MySQL dump command specified
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
@ -434,6 +444,7 @@ def test_execute_dump_command_with_duplicate_dump_skips_mysqldump():
assert (
module.execute_dump_command(
database={'name': 'foo'},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
@ -449,7 +460,7 @@ def test_execute_dump_command_with_dry_run_skips_mysqldump():
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)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').never()
@ -457,6 +468,7 @@ def test_execute_dump_command_with_dry_run_skips_mysqldump():
assert (
module.execute_dump_command(
database={'name': 'foo'},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
@ -472,7 +484,7 @@ def test_dump_data_sources_errors_for_missing_all_databases():
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
'databases/localhost/all'
)
@ -494,7 +506,7 @@ def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
'databases/localhost/all'
)
@ -519,7 +531,7 @@ def test_restore_data_source_dump_runs_mysql_to_restore():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
('mysql', '--batch'),
processes=[extract_process],
@ -550,7 +562,7 @@ def test_restore_data_source_dump_runs_mysql_with_options():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
('mysql', '--batch', '--harder'),
processes=[extract_process],
@ -581,7 +593,7 @@ def test_restore_data_source_dump_runs_non_default_mysql_with_options():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
('custom_mysql', '--batch', '--harder'),
processes=[extract_process],
@ -612,7 +624,7 @@ def test_restore_data_source_dump_runs_mysql_with_hostname_and_port():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mysql',
@ -652,7 +664,7 @@ def test_restore_data_source_dump_runs_mysql_with_username_and_password():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
('mysql', '--batch', '--user', 'root'),
processes=[extract_process],
@ -693,7 +705,7 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mysql',
@ -747,7 +759,7 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mysql',
@ -788,7 +800,7 @@ def test_restore_data_source_dump_with_dry_run_skips_restore():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').never()
module.restore_data_source_dump(

View File

@ -26,9 +26,9 @@ def test_make_extra_environment_maps_options_to_environment():
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
extra_env = module.make_extra_environment(database)
extra_env = module.make_extra_environment(database, {})
assert extra_env == expected
@ -37,10 +37,10 @@ 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)
).replace_with(lambda value, config: value)
extra = module.make_extra_environment(
database, restore_connection_params={'password': 'clipassword'}
database, {}, restore_connection_params={'password': 'clipassword'}
)
assert extra['PGPASSWORD'] == 'clipassword'
@ -50,7 +50,7 @@ def test_make_extra_environment_without_cli_password_or_configured_password_does
database = {'name': 'foo'}
extra = module.make_extra_environment(
database, restore_connection_params={'username': 'someone'}
database, {}, restore_connection_params={'username': 'someone'}
)
assert 'PGPASSWORD' not in extra
@ -59,7 +59,7 @@ def test_make_extra_environment_without_cli_password_or_configured_password_does
def test_make_extra_environment_without_ssl_mode_does_not_set_ssl_mode():
database = {'name': 'foo'}
extra = module.make_extra_environment(database)
extra = module.make_extra_environment(database, {})
assert 'PGSSLMODE' not in extra
@ -67,41 +67,41 @@ def test_make_extra_environment_without_ssl_mode_does_not_set_ssl_mode():
def test_database_names_to_dump_passes_through_individual_database_name():
database = {'name': 'foo'}
assert module.database_names_to_dump(database, flexmock(), dry_run=False) == ('foo',)
assert module.database_names_to_dump(database, {}, flexmock(), dry_run=False) == ('foo',)
def test_database_names_to_dump_passes_through_individual_database_name_with_format():
database = {'name': 'foo', 'format': 'custom'}
assert module.database_names_to_dump(database, flexmock(), dry_run=False) == ('foo',)
assert module.database_names_to_dump(database, {}, flexmock(), dry_run=False) == ('foo',)
def test_database_names_to_dump_passes_through_all_without_format():
database = {'name': 'all'}
assert module.database_names_to_dump(database, flexmock(), dry_run=False) == ('all',)
assert module.database_names_to_dump(database, {}, flexmock(), dry_run=False) == ('all',)
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)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_and_capture_output').never()
assert module.database_names_to_dump(database, flexmock(), dry_run=True) == ()
assert module.database_names_to_dump(database, {}, flexmock(), dry_run=True) == ()
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)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
'foo,test,\nbar,test,"stuff and such"'
)
assert module.database_names_to_dump(database, flexmock(), dry_run=False) == (
assert module.database_names_to_dump(database, {}, flexmock(), dry_run=False) == (
'foo',
'bar',
)
@ -111,7 +111,7 @@ def test_database_names_to_dump_with_all_and_format_lists_databases_with_hostnam
database = {'name': 'all', 'format': 'custom', 'hostname': 'localhost', 'port': 1234}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
(
'psql',
@ -128,7 +128,7 @@ def test_database_names_to_dump_with_all_and_format_lists_databases_with_hostnam
extra_environment=object,
).and_return('foo,test,\nbar,test,"stuff and such"')
assert module.database_names_to_dump(database, flexmock(), dry_run=False) == (
assert module.database_names_to_dump(database, {}, flexmock(), dry_run=False) == (
'foo',
'bar',
)
@ -138,7 +138,7 @@ def test_database_names_to_dump_with_all_and_format_lists_databases_with_usernam
database = {'name': 'all', 'format': 'custom', 'username': 'postgres'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
(
'psql',
@ -153,7 +153,7 @@ def test_database_names_to_dump_with_all_and_format_lists_databases_with_usernam
extra_environment=object,
).and_return('foo,test,\nbar,test,"stuff and such"')
assert module.database_names_to_dump(database, flexmock(), dry_run=False) == (
assert module.database_names_to_dump(database, {}, flexmock(), dry_run=False) == (
'foo',
'bar',
)
@ -163,13 +163,13 @@ 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)
).replace_with(lambda value, config: 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,
).and_return('foo,test,\nbar,test,"stuff and such"')
assert module.database_names_to_dump(database, flexmock(), dry_run=False) == (
assert module.database_names_to_dump(database, {}, flexmock(), dry_run=False) == (
'foo',
'bar',
)
@ -179,12 +179,12 @@ def test_database_names_to_dump_with_all_and_format_excludes_particular_database
database = {'name': 'all', 'format': 'custom'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
'foo,test,\ntemplate0,test,blah'
)
assert module.database_names_to_dump(database, flexmock(), dry_run=False) == ('foo',)
assert module.database_names_to_dump(database, {}, flexmock(), dry_run=False) == ('foo',)
def test_database_names_to_dump_with_all_and_psql_command_uses_custom_command():
@ -195,7 +195,7 @@ def test_database_names_to_dump_with_all_and_psql_command_uses_custom_command():
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
(
'docker',
@ -213,7 +213,7 @@ def test_database_names_to_dump_with_all_and_psql_command_uses_custom_command():
extra_environment=object,
).and_return('foo,text').once()
assert module.database_names_to_dump(database, flexmock(), dry_run=False) == ('foo',)
assert module.database_names_to_dump(database, {}, flexmock(), dry_run=False) == ('foo',)
def test_use_streaming_true_for_any_non_directory_format_databases():
@ -248,7 +248,7 @@ def test_dump_data_sources_runs_pg_dump_for_each_database():
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)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
for name, process in zip(('foo', 'bar'), processes):
@ -355,7 +355,7 @@ def test_dump_data_sources_with_dry_run_skips_pg_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)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
flexmock(module).should_receive('execute_command').never()
@ -384,7 +384,7 @@ def test_dump_data_sources_runs_pg_dump_with_hostname_and_port():
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)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@ -432,7 +432,7 @@ def test_dump_data_sources_runs_pg_dump_with_username_and_password():
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)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@ -478,7 +478,7 @@ def test_dump_data_sources_with_username_injection_attack_gets_escaped():
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)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@ -521,7 +521,7 @@ def test_dump_data_sources_runs_pg_dump_with_directory_format():
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)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_parent_directory_for_dump')
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
@ -566,7 +566,7 @@ def test_dump_data_sources_runs_pg_dump_with_options():
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)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@ -609,7 +609,7 @@ def test_dump_data_sources_runs_pg_dumpall_for_all_databases():
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)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@ -641,7 +641,7 @@ def test_dump_data_sources_runs_non_default_pg_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)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@ -679,7 +679,7 @@ def test_restore_data_source_dump_runs_pg_restore():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: 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')
@ -736,7 +736,7 @@ def test_restore_data_source_dump_runs_pg_restore_with_hostname_and_port():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: 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')
@ -801,7 +801,7 @@ def test_restore_data_source_dump_runs_pg_restore_with_username_and_password():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('make_extra_environment').and_return(
{'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'}
)
@ -875,7 +875,7 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('make_extra_environment').and_return(
{'PGPASSWORD': 'clipassword', 'PGSSLMODE': 'disable'}
)
@ -957,7 +957,7 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('make_extra_environment').and_return(
{'PGPASSWORD': 'restorepassword', 'PGSSLMODE': 'disable'}
)
@ -1033,7 +1033,7 @@ def test_restore_data_source_dump_runs_pg_restore_with_options():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: 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')
@ -1090,7 +1090,7 @@ def test_restore_data_source_dump_runs_psql_for_all_database_dump():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: 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')
@ -1132,7 +1132,7 @@ def test_restore_data_source_dump_runs_psql_for_plain_database_dump():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: 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')
@ -1186,7 +1186,7 @@ def test_restore_data_source_dump_runs_non_default_pg_restore_and_psql():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: 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')
@ -1250,7 +1250,7 @@ def test_restore_data_source_dump_with_dry_run_skips_restore():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: 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')
@ -1277,7 +1277,7 @@ def test_restore_data_source_dump_without_extract_process_restores_from_disk():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: 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')
@ -1332,7 +1332,7 @@ def test_restore_data_source_dump_with_schemas_restores_schemas():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: 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')

View File

@ -38,7 +38,7 @@ 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)
).replace_with(lambda value, config: 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),
@ -62,7 +62,7 @@ def test_ping_monitor_with_access_token_hits_hosted_ntfy_on_fail():
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: 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),
@ -88,7 +88,7 @@ def test_ping_monitor_with_username_password_and_access_token_ignores_username_p
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: 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),
@ -114,7 +114,7 @@ def test_ping_monitor_with_username_password_hits_hosted_ntfy_on_fail():
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: 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),
@ -135,7 +135,7 @@ 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)
).replace_with(lambda value, config: 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),
@ -157,7 +157,7 @@ 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)
).replace_with(lambda value, config: 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),
@ -179,7 +179,7 @@ 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)
).replace_with(lambda value, config: value)
flexmock(module.requests).should_receive('post').never()
module.ping_monitor(
@ -196,7 +196,7 @@ 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)
).replace_with(lambda value, config: value)
flexmock(module.requests).should_receive('post').never()
module.ping_monitor(
@ -213,7 +213,7 @@ 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)
).replace_with(lambda value, config: 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),
@ -234,7 +234,7 @@ 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)
).replace_with(lambda value, config: value)
flexmock(module.requests).should_receive('post').never()
module.ping_monitor(
@ -251,7 +251,7 @@ 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)
).replace_with(lambda value, config: 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()
@ -270,7 +270,7 @@ 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)
).replace_with(lambda value, config: 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),
@ -291,7 +291,7 @@ 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)
).replace_with(lambda value, config: 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),
@ -331,7 +331,7 @@ 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)
).replace_with(lambda value, config: value)
response = flexmock(ok=False)
response.should_receive('raise_for_status').and_raise(
module.requests.exceptions.RequestException

View File

@ -6,7 +6,7 @@ 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)
).replace_with(lambda value, config: value)
flexmock(module.requests).should_receive('post').never()
module.ping_monitor(
@ -22,7 +22,7 @@ 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)
).replace_with(lambda value, config: value)
flexmock(module.requests).should_receive('post').never()
module.ping_monitor(
@ -38,7 +38,7 @@ 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)
).replace_with(lambda value, config: value)
flexmock(module.requests).should_receive('post').and_return(flexmock(ok=True))
module.ping_monitor(
@ -54,7 +54,7 @@ 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)
).replace_with(lambda value, config: value)
flexmock(module.requests).should_receive('post').never()
module.ping_monitor(
@ -70,7 +70,7 @@ 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)
).replace_with(lambda value, config: value)
flexmock(module.requests).should_receive('post').and_raise(
module.requests.exceptions.ConnectionError
)
@ -107,7 +107,7 @@ 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)
).replace_with(lambda value, config: value)
response.should_receive('raise_for_status').and_raise(
module.requests.exceptions.RequestException
)

View File

@ -13,7 +13,7 @@ def test_ping_monitor_config_with_minimum_config_fail_state_backup_successfully_
hook_config = {'token': 'ksdjfwoweijfvwoeifvjmwghagy92', 'user': '983hfe0of902lkjfa2amanfgui'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').with_args(
'https://api.pushover.net/1/messages.json',
@ -43,7 +43,7 @@ def test_ping_monitor_config_with_minimum_config_start_state_backup_not_send_to_
hook_config = {'token': 'ksdjfwoweijfvwoeifvjmwghagy92', 'user': '983hfe0of902lkjfa2amanfgui'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').never()
@ -71,7 +71,7 @@ def test_ping_monitor_start_state_backup_default_message_successfully_send_to_pu
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').with_args(
'https://api.pushover.net/1/messages.json',
@ -107,7 +107,7 @@ def test_ping_monitor_start_state_backup_custom_message_successfully_send_to_pus
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').with_args(
'https://api.pushover.net/1/messages.json',
@ -142,7 +142,7 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_emergency
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').with_args(
'https://api.pushover.net/1/messages.json',
@ -180,7 +180,7 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_emergency
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').with_args(
'https://api.pushover.net/1/messages.json',
@ -218,7 +218,7 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_emergency
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').with_args(
'https://api.pushover.net/1/messages.json',
@ -259,7 +259,7 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_high_decl
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').never()
@ -314,7 +314,7 @@ def test_ping_monitor_start_state_backup_based_on_documentation_advanced_example
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').with_args(
'https://api.pushover.net/1/messages.json',
@ -380,7 +380,7 @@ def test_ping_monitor_fail_state_backup_based_on_documentation_advanced_example_
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').with_args(
'https://api.pushover.net/1/messages.json',
@ -451,7 +451,7 @@ def test_ping_monitor_finish_state_backup_based_on_documentation_advanced_exampl
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').with_args(
'https://api.pushover.net/1/messages.json',
@ -487,7 +487,7 @@ def test_ping_monitor_config_with_minimum_config_fail_state_backup_successfully_
hook_config = {'token': 'ksdjfwoweijfvwoeifvjmwghagy92', 'user': '983hfe0of902lkjfa2amanfgui'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').and_return(flexmock(ok=True)).never()
@ -511,7 +511,7 @@ def test_ping_monitor_config_incorrect_state_exit_early():
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').and_return(flexmock(ok=True)).never()
@ -537,7 +537,7 @@ def test_ping_monitor_push_post_error_bails():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
push_response = flexmock(ok=False)
push_response.should_receive('raise_for_status').and_raise(
module.requests.ConnectionError

View File

@ -77,7 +77,7 @@ def test_ping_monitor_config_with_api_key_only_bails():
hook_config = {'api_key': API_KEY}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
@ -97,7 +97,7 @@ def test_ping_monitor_config_with_host_only_bails():
hook_config = {'host': HOST}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
@ -117,7 +117,7 @@ def test_ping_monitor_config_with_key_only_bails():
hook_config = {'key': KEY}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
@ -137,7 +137,7 @@ def test_ping_monitor_config_with_server_only_bails():
hook_config = {'server': SERVER}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
@ -156,7 +156,7 @@ def test_ping_monitor_config_user_password_no_zabbix_data_bails():
hook_config = {'server': SERVER, 'username': USERNAME, 'password': PASSWORD}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
@ -175,7 +175,7 @@ def test_ping_monitor_config_api_key_no_zabbix_data_bails():
hook_config = {'server': SERVER, 'api_key': API_KEY}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
@ -195,7 +195,7 @@ def test_ping_monitor_config_itemid_no_auth_data_bails():
hook_config = {'server': SERVER, 'itemid': ITEMID}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
@ -215,7 +215,7 @@ def test_ping_monitor_config_host_and_key_no_auth_data_bails():
hook_config = {'server': SERVER, 'host': HOST, 'key': KEY}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
@ -235,7 +235,7 @@ def test_ping_monitor_config_host_and_key_with_api_key_auth_data_successful():
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)
).replace_with(lambda value, config: value)
flexmock(module.requests).should_receive('post').with_args(
f'{SERVER}',
headers=AUTH_HEADERS_API_KEY,
@ -257,7 +257,7 @@ 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)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
@ -275,7 +275,7 @@ 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)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
@ -302,7 +302,7 @@ def test_ping_monitor_config_host_and_key_with_username_password_auth_data_succe
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
auth_response = flexmock(ok=True)
auth_response.should_receive('json').and_return(
{'jsonrpc': '2.0', 'result': '3fe6ed01a69ebd79907a120bcd04e494', 'id': 1}
@ -343,7 +343,7 @@ def test_ping_monitor_config_host_and_key_with_username_password_auth_data_and_a
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
auth_response = flexmock(ok=False)
auth_response.should_receive('json').and_return(
{'jsonrpc': '2.0', 'result': '3fe6ed01a69ebd79907a120bcd04e494', 'id': 1}
@ -384,7 +384,7 @@ def test_ping_monitor_config_host_and_key_with_username_and_missing_password_bai
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
@ -408,7 +408,7 @@ def test_ping_monitor_config_host_and_key_with_password_and_missing_username_bai
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
@ -428,7 +428,7 @@ def test_ping_monitor_config_itemid_with_api_key_auth_data_successful():
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)
).replace_with(lambda value, config: value)
flexmock(module.requests).should_receive('post').with_args(
f'{SERVER}',
headers=AUTH_HEADERS_API_KEY,
@ -453,7 +453,7 @@ def test_ping_monitor_config_itemid_with_username_password_auth_data_successful(
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
auth_response = flexmock(ok=True)
auth_response.should_receive('json').and_return(
{'jsonrpc': '2.0', 'result': '3fe6ed01a69ebd79907a120bcd04e494', 'id': 1}
@ -488,7 +488,7 @@ def test_ping_monitor_config_itemid_with_username_password_auth_data_and_push_po
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
auth_response = flexmock(ok=True)
auth_response.should_receive('json').and_return(
{'jsonrpc': '2.0', 'result': '3fe6ed01a69ebd79907a120bcd04e494', 'id': 1}