Compare commits

..
45 Commits
Author SHA1 Message Date
Dan Helfman fbdb09b87d Bump version for release. 2025-03-10 10:17:36 -07:00
Dan Helfman bec5a0c0ca Fix end-to-end tests for Btrfs (#1023). 2025-03-10 10:15:23 -07:00
Dan Helfman 4ee7f72696 Fix an error in the Btrfs hook when attempting to snapshot a read-only subvolume (#1023). 2025-03-09 23:04:55 -07:00
Dan Helfman 68b6d01071 Fix a regression in which the "exclude_patterns" option didn't expand "~" (#1021). 2025-03-09 13:35:22 -07:00
Dan Helfman 86b138e73b Clarify command hook documentation. 2025-03-08 21:00:58 -08:00
Dan Helfman c76a108422 Link to Zabbix documentation from NEWS. 2025-03-06 10:37:00 -08:00
Dan Helfman eb5dc128bf Fix incorrect test name (#1017). 2025-03-06 10:34:28 -08:00
Dan Helfman 1d486d024b Fix a regression in which some MariaDB/MySQL passwords were not escaped correctly (#1017). 2025-03-06 10:32:38 -08:00
Dan Helfman 5a8f27d75c Add single quotes around the MariaDB password (#1017).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/1017
2025-03-06 18:01:43 +00:00
Geoff Holden a926b413bc Updating automated test, and fixing linting errors. 2025-03-06 09:00:33 -03:30
Geoff Holden 18ffd96d62 Add single quotes around the password.
When the DB password uses some special characters, the
defaults-extra-file can be incorrect. In the case of a password with
the # symbol, anything after that is considered a comment. The single
quotes around the password rectify this.
2025-03-05 22:51:41 -03:30
Dan Helfman c0135864c2 With the PagerDuty monitoring hook, send borgmatic logs to PagerDuty so they show up in the incident UI (#409). 2025-03-04 08:55:09 -08:00
Dan Helfman ddfd3c6ca1 Clarify Zabbix monitoring hook documentation about creating items (#936). 2025-03-03 16:02:22 -08:00
Dan Helfman dbe82ff11e Bump version for release. 2025-03-03 10:21:15 -08:00
Dan Helfman 55c0ab1610 Add "tls" options to the MariaDB and MySQL database hooks. 2025-03-03 10:07:03 -08:00
Dan Helfman 1f86100f26 NEWS wording tweaks. 2025-03-02 20:10:20 -08:00
Dan Helfman 2a16ffab1b When ctrl-C is pressed, ensure Borg actually exits (#1015). 2025-03-02 10:32:57 -08:00
Dan Helfman 4b2f7e03af Fix broken "config generate" (#975). 2025-03-01 21:02:32 -08:00
Dan Helfman 024006f4c0 Title case Borg. 2025-03-01 20:56:40 -08:00
Dan Helfman 4c71e600ca Expand a little on the specifics of backups of an LVM volume (#1014).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/1014
2025-03-02 04:55:13 +00:00
lingfish 114f5702b2 Expand a little on the specifics of backups of an LVM volume. 2025-03-02 14:22:57 +11:00
Dan Helfman 54afe87a9f Add a "compression" option to the PostgreSQL database hook (#975). 2025-03-01 17:29:16 -08:00
Dan Helfman 25b6a49df7 Send database passwords to MongoDB via anonymous pipe (#1013). 2025-03-01 10:04:04 -08:00
Dan Helfman b97372adf2 Add MariaDB and MySQL anonymous pipe to NEWS (#1009). 2025-03-01 08:49:42 -08:00
Dan Helfman 6bc9a592d9 Send MariaDB and MySQL passwords via anonymous pipe instead of environment variable (#1009).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/1011
2025-03-01 03:33:08 +00:00
Dan Helfman 839862cff0 Update documentation link text about providing database passwords from external sources (#1009). 2025-02-28 19:31:22 -08:00
Dan Helfman 06b065cb09 Add missing test coverage (#1009). 2025-02-28 18:28:09 -08:00
Dan Helfman 1e5c256d54 Get tests passing again (#1009). 2025-02-28 14:40:00 -08:00
Dan Helfman baf5fec78d If the user supplies their own --defaults-extra-file, include it from the one we generate (#1009). 2025-02-28 10:53:17 -08:00
Dan Helfman 48a4fbaa89 Add missing test coverage for defaults file function (#1009). 2025-02-28 09:21:01 -08:00
Dan Helfman 1e274d7153 Add some missing test mocking (#1009). 2025-02-28 08:59:38 -08:00
Dan Helfman c41b743819 Get existing unit tests passing (#1009). 2025-02-28 08:37:03 -08:00
Dan Helfman 36d0073375 Send MySQL passwords via anonymous pipe instead of environment variable (#1009). 2025-02-27 10:42:47 -08:00
Dan Helfman 0bd418836e Send MariaDB passwords via anonymous pipe instead of environment variable (#1009) 2025-02-27 10:15:45 -08:00
Dan Helfman 923fa7d82f Include contributors of closed tickets in "recent contributors" documentation. 2025-02-27 09:23:08 -08:00
Dan Helfman dce0528057 In the Zabbix monitoring hook, support Zabbix 7.2's authentication changes (#1003). 2025-02-26 22:33:01 -08:00
Dan Helfman 8a6c6c84d2 Add Uptime Kuma "verify_tls" option to NEWS. 2025-02-24 11:30:16 -08:00
Dan HelfmanandGitHub 1e21c8f97b Add "verify_tls" option to Uptime Kuma hook.
Merge pull request #90 from columbarius/uptimekuma-verify-tls
2025-02-24 11:28:18 -08:00
columbarius 2eab74a521 Add "verify_tls" option to Uptime Kuma hook. 2025-02-24 20:12:47 +01:00
Dan Helfman 3bca686707 Fix a ZFS error during snapshot cleanup (#1001). 2025-02-23 17:01:35 -08:00
Dan Helfman 8854b9ad20 Backing out a ZFS change that hasn't been confirmed working quite yet. 2025-02-23 15:49:12 -08:00
Dan Helfman bcc463688a When getting all ZFS dataset mount points, deduplicate and filter out "none". 2025-02-23 15:46:39 -08:00
Dan Helfman 596305e3de Bump version for release. 2025-02-23 09:59:53 -08:00
Dan Helfman c462f0c84c Fix Python < 3.12 compatibility issue (#1005). 2025-02-23 09:59:19 -08:00
Dan Helfman 4f0142c3c5 Fix Python < 3.12 compatibility issue (#1005). 2025-02-23 09:09:47 -08:00
40 changed files with 2044 additions and 469 deletions
+29
View File
@@ -1,3 +1,32 @@
1.9.14
* #409: With the PagerDuty monitoring hook, send borgmatic logs to PagerDuty so they show up in the
incident UI. See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook
* #936: Clarify Zabbix monitoring hook documentation about creating items:
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#zabbix-hook
* #1017: Fix a regression in which some MariaDB/MySQL passwords were not escaped correctly.
* #1021: Fix a regression in which the "exclude_patterns" option didn't expand "~" (the user's
home directory). This fix means that all "patterns" and "patterns_from" also now expand "~".
* #1023: Fix an error in the Btrfs hook when attempting to snapshot a read-only subvolume. Now,
read-only subvolumes are ignored since Btrfs can't actually snapshot them.
1.9.13
* #975: Add a "compression" option to the PostgreSQL database hook.
* #1001: Fix a ZFS error during snapshot cleanup.
* #1003: In the Zabbix monitoring hook, support Zabbix 7.2's authentication changes.
* #1009: Send database passwords to MariaDB and MySQL via anonymous pipe, which is more secure than
using an environment variable.
* #1013: Send database passwords to MongoDB via anonymous pipe, which is more secure than using
"--password" on the command-line!
* #1015: When ctrl-C is pressed, more strongly encourage Borg to actually exit.
* Add a "verify_tls" option to the Uptime Kuma monitoring hook for disabling TLS verification.
* Add "tls" options to the MariaDB and MySQL database hooks to enable or disable TLS encryption
between client and server.
1.9.12
* #1005: Fix the credential hooks to avoid using Python 3.12+ string features. Now borgmatic will
work with Python 3.9, 3.10, and 3.11 again.
1.9.11
* #795: Add credential loading from file, KeePassXC, and Docker/Podman secrets. See the
documentation for more information:
+14 -3
View File
@@ -130,8 +130,11 @@ def expand_directory(directory, working_directory):
def expand_patterns(patterns, working_directory=None, skip_paths=None):
'''
Given a sequence of borgmatic.borg.pattern.Pattern instances and an optional working directory,
expand tildes and globs in each root pattern. Return all the resulting patterns (not just the
root patterns) as a tuple.
expand tildes and globs in each root pattern and expand just tildes in each non-root pattern.
The idea is that non-root patterns may be regular expressions or other pattern styles containing
"*" that borgmatic should not expand as a shell glob.
Return all the resulting patterns as a tuple.
If a set of paths are given to skip, then don't expand any patterns matching them.
'''
@@ -153,7 +156,15 @@ def expand_patterns(patterns, working_directory=None, skip_paths=None):
)
if pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT
and pattern.path not in (skip_paths or ())
else (pattern,)
else (
borgmatic.borg.pattern.Pattern(
os.path.expanduser(pattern.path),
pattern.type,
pattern.style,
pattern.device,
pattern.source,
),
)
)
for pattern in patterns
)
+1 -1
View File
@@ -74,7 +74,7 @@ def make_environment(config):
os.write(write_file_descriptor, passphrase.encode('utf-8'))
os.close(write_file_descriptor)
# This, plus subprocess.Popen(..., close_fds=False) in execute.py, is necessary for the Borg
# This plus subprocess.Popen(..., close_fds=False) in execute.py is necessary for the Borg
# child process to inherit the file descriptor.
os.set_inheritable(read_file_descriptor, True)
environment['BORG_PASSPHRASE_FD'] = str(read_file_descriptor)
+1
View File
@@ -41,6 +41,7 @@ def schema_to_sample_configuration(schema, level=0, parent_is_sequence=False):
'''
schema_type = schema.get('type')
example = schema.get('example')
if example is not None:
return example
+56 -1
View File
@@ -1040,6 +1040,18 @@ properties:
individual databases. See the pg_dump documentation for
more about formats.
example: directory
compression:
type: ["string", "integer"]
description: |
Database dump compression level (integer) or method
("gzip", "lz4", "zstd", or "none") and optional
colon-separated detail. Defaults to moderate "gzip" for
"custom" and "directory" formats and no compression for
the "plain" format. Compression is not supported for the
"tar" format. Be aware that Borg does its own
compression as well, so you may not need it in both
places.
example: none
ssl_mode:
type: string
enum: ['disable', 'allow', 'prefer',
@@ -1198,6 +1210,20 @@ properties:
Defaults to the "password" option. Supports the
"{credential ...}" syntax.
example: trustsome1
tls:
type: boolean
description: |
Whether to TLS-encrypt data transmitted between the
client and server. The default varies based on the
MariaDB version.
example: false
restore_tls:
type: boolean
description: |
Whether to TLS-encrypt data transmitted between the
client and restore server. The default varies based on
the MariaDB version.
example: false
mariadb_dump_command:
type: string
description: |
@@ -1328,6 +1354,20 @@ properties:
Defaults to the "password" option. Supports the
"{credential ...}" syntax.
example: trustsome1
tls:
type: boolean
description: |
Whether to TLS-encrypt data transmitted between the
client and server. The default varies based on the
MySQL installation.
example: false
restore_tls:
type: boolean
description: |
Whether to TLS-encrypt data transmitted between the
client and restore server. The default varies based on
the MySQL installation.
example: false
mysql_dump_command:
type: string
description: |
@@ -1907,6 +1947,8 @@ properties:
zabbix:
type: object
additionalProperties: false
required:
- server
properties:
itemid:
type: integer
@@ -1929,7 +1971,8 @@ properties:
server:
type: string
description: |
The address of your Zabbix instance.
The API endpoint URL of your Zabbix instance, usually ending
with "/api_jsonrpc.php". Required.
example: https://zabbix.your-domain.com
username:
type: string
@@ -2200,6 +2243,12 @@ properties:
- start
- finish
- fail
verify_tls:
type: boolean
description: |
Verify the TLS certificate of the push URL host. Defaults to
true.
example: false
description: |
Configuration for a monitoring integration with Uptime Kuma using
the Push monitor type.
@@ -2230,6 +2279,12 @@ properties:
PagerDuty integration key used to notify PagerDuty when a
backup errors. Supports the "{credential ...}" syntax.
example: a177cad45bd374409f78906a810a3074
send_logs:
type: boolean
description: |
Send borgmatic logs to PagerDuty when a backup errors.
Defaults to true.
example: false
description: |
Configuration for a monitoring integration with PagerDuty. Create an
account at https://www.pagerduty.com if you'd like to use this
+19 -16
View File
@@ -266,8 +266,8 @@ def log_command(full_command, input_file=None, output_file=None, environment=Non
width=MAX_LOGGED_COMMAND_LENGTH,
placeholder=' ...',
)
+ (f" < {getattr(input_file, 'name', '')}" if input_file else '')
+ (f" > {getattr(output_file, 'name', '')}" if output_file else '')
+ (f" < {getattr(input_file, 'name', input_file)}" if input_file else '')
+ (f" > {getattr(output_file, 'name', output_file)}" if output_file else '')
)
@@ -315,8 +315,8 @@ def execute_command(
shell=shell,
env=environment,
cwd=working_directory,
# Necessary for the passcommand credential hook to work.
close_fds=not bool((environment or {}).get('BORG_PASSPHRASE_FD')),
# Necessary for passing credentials via anonymous pipe.
close_fds=False,
)
if not run_to_completion:
return process
@@ -333,6 +333,7 @@ def execute_command(
def execute_command_and_capture_output(
full_command,
input_file=None,
capture_stderr=False,
shell=False,
environment=None,
@@ -342,28 +343,30 @@ def execute_command_and_capture_output(
):
'''
Execute the given command (a sequence of command/argument strings), capturing and returning its
output (stdout). If capture stderr is True, then capture and return stderr in addition to
stdout. If shell is True, execute the command within a shell. If an environment variables dict
is given, then pass it into the command. If a working directory is given, use that as the
present working directory when running the command. If a Borg local path is given, and the
command matches it (regardless of arguments), treat exit code 1 as a warning instead of an
error. But if Borg exit codes are given as a sequence of exit code configuration dicts, then use
that configuration to decide what's an error and what's a warning.
output (stdout). If an input file descriptor is given, then pipe it to the command's stdin. If
capture stderr is True, then capture and return stderr in addition to stdout. If shell is True,
execute the command within a shell. If an environment variables dict is given, then pass it into
the command. If a working directory is given, use that as the present working directory when
running the command. If a Borg local path is given, and the command matches it (regardless of
arguments), treat exit code 1 as a warning instead of an error. But if Borg exit codes are given
as a sequence of exit code configuration dicts, then use that configuration to decide what's an
error and what's a warning.
Raise subprocesses.CalledProcessError if an error occurs while running the command.
'''
log_command(full_command, environment=environment)
log_command(full_command, input_file, environment=environment)
command = ' '.join(full_command) if shell else full_command
try:
output = subprocess.check_output(
command,
stdin=input_file,
stderr=subprocess.STDOUT if capture_stderr else None,
shell=shell,
env=environment,
cwd=working_directory,
# Necessary for the passcommand credential hook to work.
close_fds=not bool((environment or {}).get('BORG_PASSPHRASE_FD')),
# Necessary for passing credentials via anonymous pipe.
close_fds=False,
)
except subprocess.CalledProcessError as error:
if (
@@ -422,8 +425,8 @@ def execute_command_with_processes(
shell=shell,
env=environment,
cwd=working_directory,
# Necessary for the passcommand credential hook to work.
close_fds=not bool((environment or {}).get('BORG_PASSPHRASE_FD')),
# Necessary for passing credentials via anonymous pipe.
close_fds=False,
)
except (subprocess.CalledProcessError, OSError):
# Something has gone wrong. So vent each process' output buffer to prevent it from hanging.
+3 -1
View File
@@ -21,7 +21,9 @@ def load_credential(hook_config, config, credential_parameters):
try:
(secret_name,) = credential_parameters
except ValueError:
raise ValueError(f'Cannot load invalid secret name: "{' '.join(credential_parameters)}"')
name = ' '.join(credential_parameters)
raise ValueError(f'Cannot load invalid secret name: "{name}"')
if not SECRET_NAME_PATTERN.match(secret_name):
raise ValueError(f'Cannot load invalid secret name: "{secret_name}"')
+3 -1
View File
@@ -15,7 +15,9 @@ def load_credential(hook_config, config, credential_parameters):
try:
(credential_path,) = credential_parameters
except ValueError:
raise ValueError(f'Cannot load invalid credential: "{' '.join(credential_parameters)}"')
name = ' '.join(credential_parameters)
raise ValueError(f'Cannot load invalid credential: "{name}"')
try:
with open(
+3 -1
View File
@@ -18,8 +18,10 @@ def load_credential(hook_config, config, credential_parameters):
try:
(database_path, attribute_name) = credential_parameters
except ValueError:
path_and_name = ' '.join(credential_parameters)
raise ValueError(
f'Cannot load credential with invalid KeePassXC database path and attribute name: "{' '.join(credential_parameters)}"'
f'Cannot load credential with invalid KeePassXC database path and attribute name: "{path_and_name}"'
)
if not os.path.exists(database_path):
+3 -3
View File
@@ -20,9 +20,9 @@ def load_credential(hook_config, config, credential_parameters):
try:
(credential_name,) = credential_parameters
except ValueError:
raise ValueError(
f'Cannot load invalid credential name: "{' '.join(credential_parameters)}"'
)
name = ' '.join(credential_parameters)
raise ValueError(f'Cannot load invalid credential name: "{name}"')
credentials_directory = os.environ.get('CREDENTIALS_DIRECTORY')
+46 -1
View File
@@ -48,6 +48,47 @@ def get_subvolume_mount_points(findmnt_command):
Subvolume = collections.namedtuple('Subvolume', ('path', 'contained_patterns'), defaults=((),))
def get_subvolume_property(btrfs_command, subvolume_path, property_name):
output = borgmatic.execute.execute_command_and_capture_output(
tuple(btrfs_command.split(' '))
+ (
'property',
'get',
'-t', # Type.
'subvol',
subvolume_path,
property_name,
),
)
try:
value = output.strip().split('=')[1]
except IndexError:
raise ValueError(f'Invalid {btrfs_command} property output')
return {
'true': True,
'false': False,
}.get(value, value)
def omit_read_only_subvolume_mount_points(btrfs_command, subvolume_paths):
'''
Given a Btrfs command to run and a sequence of Btrfs subvolume mount points, filter them down to
just those that are read-write. The idea is that Btrfs can't actually snapshot a read-only
subvolume, so we should just ignore them.
'''
retained_subvolume_paths = []
for subvolume_path in subvolume_paths:
if get_subvolume_property(btrfs_command, subvolume_path, 'ro'):
logger.debug(f'Ignoring Btrfs subvolume {subvolume_path} because it is read-only')
else:
retained_subvolume_paths.append(subvolume_path)
return tuple(retained_subvolume_paths)
def get_subvolumes(btrfs_command, findmnt_command, patterns=None):
'''
Given a Btrfs command to run and a sequence of configured patterns, find the intersection
@@ -67,7 +108,11 @@ def get_subvolumes(btrfs_command, findmnt_command, patterns=None):
# backup. Sort the subvolumes from longest to shortest mount points, so longer mount points get
# a whack at the candidate pattern piñata before their parents do. (Patterns are consumed during
# this process, so no two subvolumes end up with the same contained patterns.)
for mount_point in reversed(get_subvolume_mount_points(findmnt_command)):
for mount_point in reversed(
omit_read_only_subvolume_mount_points(
btrfs_command, get_subvolume_mount_points(findmnt_command)
)
):
subvolumes.extend(
Subvolume(mount_point, contained_patterns)
for contained_patterns in (
+128 -43
View File
@@ -1,6 +1,7 @@
import copy
import logging
import os
import re
import shlex
import borgmatic.borg.pattern
@@ -23,14 +24,92 @@ def make_dump_path(base_directory): # pragma: no cover
return dump.make_data_source_dump_path(base_directory, 'mariadb_databases')
SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys')
DEFAULTS_EXTRA_FILE_FLAG_PATTERN = re.compile('^--defaults-extra-file=(?P<filename>.*)$')
def database_names_to_dump(database, config, environment, dry_run):
def parse_extra_options(extra_options):
'''
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.
Given an extra options string, split the options into a tuple and return it. Additionally, if
the first option is "--defaults-extra-file=...", then remove it from the options and return the
filename.
So the return value is a tuple of: (parsed options, defaults extra filename).
The intent is to support downstream merging of multiple "--defaults-extra-file"s, as
MariaDB/MySQL only allows one at a time.
'''
split_extra_options = tuple(shlex.split(extra_options)) if extra_options else ()
if not split_extra_options:
return ((), None)
match = DEFAULTS_EXTRA_FILE_FLAG_PATTERN.match(split_extra_options[0])
if not match:
return (split_extra_options, None)
return (split_extra_options[1:], match.group('filename'))
def make_defaults_file_options(username=None, password=None, defaults_extra_filename=None):
'''
Given a database username and/or password, write it to an anonymous pipe and return the flags
for passing that file descriptor to an executed command. The idea is that this is a more secure
way to transmit credentials to a database client than using an environment variable.
If no username or password are given, then return the options for the given defaults extra
filename (if any). But if there is a username and/or password and a defaults extra filename is
given, then "!include" it from the generated file, effectively allowing multiple defaults extra
files.
Do not use the returned value for multiple different command invocations. That will not work
because each pipe is "used up" once read.
'''
escaped_password = None if password is None else password.replace('\\', '\\\\')
values = '\n'.join(
(
(f'user={username}' if username is not None else ''),
(f'password="{escaped_password}"' if escaped_password is not None else ''),
)
).strip()
if not values:
if defaults_extra_filename:
return (f'--defaults-extra-file={defaults_extra_filename}',)
return ()
fields_message = ' and '.join(
field_name
for field_name in (
(f'username ({username})' if username is not None else None),
('password' if password is not None else None),
)
if field_name is not None
)
include_message = f' (including {defaults_extra_filename})' if defaults_extra_filename else ''
logger.debug(f'Writing database {fields_message} to defaults extra file pipe{include_message}')
include = f'!include {defaults_extra_filename}\n' if defaults_extra_filename else ''
read_file_descriptor, write_file_descriptor = os.pipe()
os.write(write_file_descriptor, f'{include}[client]\n{values}'.encode('utf-8'))
os.close(write_file_descriptor)
# This plus subprocess.Popen(..., close_fds=False) in execute.py is necessary for the database
# client child process to inherit the file descriptor.
os.set_inheritable(read_file_descriptor, True)
return (f'--defaults-extra-file=/dev/fd/{read_file_descriptor}',)
def database_names_to_dump(database, config, username, password, environment, dry_run):
'''
Given a requested database config, a configuration dict, a database username and password, an
environment dict, and whether this is a dry run, 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'],)
@@ -40,24 +119,22 @@ def database_names_to_dump(database, config, environment, dry_run):
mariadb_show_command = tuple(
shlex.quote(part) for part in shlex.split(database.get('mariadb_command') or 'mariadb')
)
extra_options, defaults_extra_filename = parse_extra_options(database.get('list_options'))
show_command = (
mariadb_show_command
+ (tuple(database['list_options'].split(' ')) if 'list_options' in database else ())
+ make_defaults_file_options(username, password, defaults_extra_filename)
+ extra_options
+ (('--host', database['hostname']) if 'hostname' in database else ())
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
+ (
(
'--user',
borgmatic.hooks.credential.parse.resolve_credential(database['username'], config),
)
if 'username' in database
else ()
)
+ (('--ssl',) if database.get('tls') is True else ())
+ (('--skip-ssl',) if database.get('tls') is False else ())
+ ('--skip-column-names', '--batch')
+ ('--execute', 'show schemas')
)
logger.debug('Querying for "all" MariaDB databases to dump')
show_output = execute_command_and_capture_output(show_command, environment=environment)
return tuple(
@@ -67,8 +144,19 @@ def database_names_to_dump(database, config, environment, dry_run):
)
SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys')
def execute_dump_command(
database, config, dump_path, database_names, environment, dry_run, dry_run_label
database,
config,
username,
password,
dump_path,
database_names,
environment,
dry_run,
dry_run_label,
):
'''
Kick off a dump for the given MariaDB database (provided as a configuration dict) to a named
@@ -95,21 +183,17 @@ def execute_dump_command(
shlex.quote(part)
for part in shlex.split(database.get('mariadb_dump_command') or 'mariadb-dump')
)
extra_options, defaults_extra_filename = parse_extra_options(database.get('options'))
dump_command = (
mariadb_dump_command
+ (tuple(database['options'].split(' ')) if 'options' in database else ())
+ make_defaults_file_options(username, password, defaults_extra_filename)
+ extra_options
+ (('--add-drop-database',) if database.get('add_drop_database', True) else ())
+ (('--host', database['hostname']) if 'hostname' in database else ())
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
+ (
(
'--user',
borgmatic.hooks.credential.parse.resolve_credential(database['username'], config),
)
if 'username' in database
else ()
)
+ (('--ssl',) if database.get('tls') is True else ())
+ (('--skip-ssl',) if database.get('tls') is False else ())
+ ('--databases',)
+ database_names
+ ('--result-file', dump_filename)
@@ -165,19 +249,16 @@ def dump_data_sources(
for database in databases:
dump_path = make_dump_path(borgmatic_runtime_directory)
environment = dict(
os.environ,
**(
{
'MYSQL_PWD': borgmatic.hooks.credential.parse.resolve_credential(
database['password'], config
)
}
if 'password' in database
else {}
),
username = borgmatic.hooks.credential.parse.resolve_credential(
database.get('username'), config
)
password = borgmatic.hooks.credential.parse.resolve_credential(
database.get('password'), config
)
environment = dict(os.environ)
dump_database_names = database_names_to_dump(
database, config, username, password, environment, dry_run
)
dump_database_names = database_names_to_dump(database, config, environment, dry_run)
if not dump_database_names:
if dry_run:
@@ -193,6 +274,8 @@ def dump_data_sources(
execute_dump_command(
renamed_database,
config,
username,
password,
dump_path,
(dump_name,),
environment,
@@ -205,6 +288,8 @@ def dump_data_sources(
execute_dump_command(
database,
config,
username,
password,
dump_path,
dump_database_names,
environment,
@@ -278,6 +363,7 @@ def restore_data_source_dump(
port = str(
connection_params['port'] or data_source.get('restore_port', data_source.get('port', ''))
)
tls = data_source.get('restore_tls', data_source.get('tls'))
username = borgmatic.hooks.credential.parse.resolve_credential(
(
connection_params['username']
@@ -296,20 +382,19 @@ def restore_data_source_dump(
mariadb_restore_command = tuple(
shlex.quote(part) for part in shlex.split(data_source.get('mariadb_command') or 'mariadb')
)
extra_options, defaults_extra_filename = parse_extra_options(data_source.get('restore_options'))
restore_command = (
mariadb_restore_command
+ make_defaults_file_options(username, password, defaults_extra_filename)
+ extra_options
+ ('--batch',)
+ (
tuple(data_source['restore_options'].split(' '))
if 'restore_options' in data_source
else ()
)
+ (('--host', hostname) if hostname else ())
+ (('--port', str(port)) if port else ())
+ (('--protocol', 'tcp') if hostname or port else ())
+ (('--user', username) if username else ())
+ (('--ssl',) if tls is True else ())
+ (('--skip-ssl',) if tls is False else ())
)
environment = dict(os.environ, **({'MYSQL_PWD': password} if password else {}))
environment = dict(os.environ)
logger.debug(f"Restoring MariaDB database {data_source['name']}{dry_run_label}")
if dry_run:
+26 -13
View File
@@ -89,12 +89,36 @@ def dump_data_sources(
return processes
def make_password_config_file(password):
'''
Given a database password, write it as a MongoDB configuration file to an anonymous pipe and
return its filename. The idea is that this is a more secure way to transmit a password to
MongoDB than providing it directly on the command-line.
Do not use the returned value for multiple different command invocations. That will not work
because each pipe is "used up" once read.
'''
logger.debug('Writing MongoDB password to configuration file pipe')
read_file_descriptor, write_file_descriptor = os.pipe()
os.write(write_file_descriptor, f'password: {password}'.encode('utf-8'))
os.close(write_file_descriptor)
# This plus subprocess.Popen(..., close_fds=False) in execute.py is necessary for the database
# client child process to inherit the file descriptor.
os.set_inheritable(read_file_descriptor, True)
return f'/dev/fd/{read_file_descriptor}'
def build_dump_command(database, config, dump_filename, dump_format):
'''
Return the mongodump command from a single database configuration.
'''
all_databases = database['name'] == 'all'
password = borgmatic.hooks.credential.parse.resolve_credential(database.get('password'), config)
return (
('mongodump',)
+ (('--out', shlex.quote(dump_filename)) if dump_format == 'directory' else ())
@@ -112,18 +136,7 @@ def build_dump_command(database, config, dump_filename, dump_format):
if 'username' in database
else ()
)
+ (
(
'--password',
shlex.quote(
borgmatic.hooks.credential.parse.resolve_credential(
database['password'], config
)
),
)
if 'password' in database
else ()
)
+ (('--config', make_password_config_file(password)) if password else ())
+ (
('--authenticationDatabase', shlex.quote(database['authentication_database']))
if 'authentication_database' in database
@@ -251,7 +264,7 @@ def build_restore_command(extract_process, database, config, dump_filename, conn
if username:
command.extend(('--username', username))
if password:
command.extend(('--password', password))
command.extend(('--config', make_password_config_file(password)))
if 'authentication_database' in database:
command.extend(('--authenticationDatabase', database['authentication_database']))
if 'restore_options' in database:
+59 -42
View File
@@ -6,6 +6,7 @@ import shlex
import borgmatic.borg.pattern
import borgmatic.config.paths
import borgmatic.hooks.credential.parse
import borgmatic.hooks.data_source.mariadb
from borgmatic.execute import (
execute_command,
execute_command_and_capture_output,
@@ -26,11 +27,12 @@ def make_dump_path(base_directory): # pragma: no cover
SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys')
def database_names_to_dump(database, config, environment, dry_run):
def database_names_to_dump(database, config, username, password, environment, dry_run):
'''
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.
Given a requested database config, a configuration dict, a database username and password, an
environment dict, and whether this is a dry run, 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'],)
@@ -40,24 +42,26 @@ def database_names_to_dump(database, config, environment, dry_run):
mysql_show_command = tuple(
shlex.quote(part) for part in shlex.split(database.get('mysql_command') or 'mysql')
)
extra_options, defaults_extra_filename = (
borgmatic.hooks.data_source.mariadb.parse_extra_options(database.get('list_options'))
)
show_command = (
mysql_show_command
+ (tuple(database['list_options'].split(' ')) if 'list_options' in database else ())
+ borgmatic.hooks.data_source.mariadb.make_defaults_file_options(
username, password, defaults_extra_filename
)
+ extra_options
+ (('--host', database['hostname']) if 'hostname' in database else ())
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
+ (
(
'--user',
borgmatic.hooks.credential.parse.resolve_credential(database['username'], config),
)
if 'username' in database
else ()
)
+ (('--ssl',) if database.get('tls') is True else ())
+ (('--skip-ssl',) if database.get('tls') is False else ())
+ ('--skip-column-names', '--batch')
+ ('--execute', 'show schemas')
)
logger.debug('Querying for "all" MySQL databases to dump')
show_output = execute_command_and_capture_output(show_command, environment=environment)
return tuple(
@@ -68,7 +72,15 @@ def database_names_to_dump(database, config, environment, dry_run):
def execute_dump_command(
database, config, dump_path, database_names, environment, dry_run, dry_run_label
database,
config,
username,
password,
dump_path,
database_names,
environment,
dry_run,
dry_run_label,
):
'''
Kick off a dump for the given MySQL/MariaDB database (provided as a configuration dict) to a
@@ -94,21 +106,21 @@ def execute_dump_command(
mysql_dump_command = tuple(
shlex.quote(part) for part in shlex.split(database.get('mysql_dump_command') or 'mysqldump')
)
extra_options, defaults_extra_filename = (
borgmatic.hooks.data_source.mariadb.parse_extra_options(database.get('options'))
)
dump_command = (
mysql_dump_command
+ (tuple(database['options'].split(' ')) if 'options' in database else ())
+ borgmatic.hooks.data_source.mariadb.make_defaults_file_options(
username, password, defaults_extra_filename
)
+ extra_options
+ (('--add-drop-database',) if database.get('add_drop_database', True) else ())
+ (('--host', database['hostname']) if 'hostname' in database else ())
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
+ (
(
'--user',
borgmatic.hooks.credential.parse.resolve_credential(database['username'], config),
)
if 'username' in database
else ()
)
+ (('--ssl',) if database.get('tls') is True else ())
+ (('--skip-ssl',) if database.get('tls') is False else ())
+ ('--databases',)
+ database_names
+ ('--result-file', dump_filename)
@@ -164,19 +176,16 @@ def dump_data_sources(
for database in databases:
dump_path = make_dump_path(borgmatic_runtime_directory)
environment = dict(
os.environ,
**(
{
'MYSQL_PWD': borgmatic.hooks.credential.parse.resolve_credential(
database['password'], config
)
}
if 'password' in database
else {}
),
username = borgmatic.hooks.credential.parse.resolve_credential(
database.get('username'), config
)
password = borgmatic.hooks.credential.parse.resolve_credential(
database.get('password'), config
)
environment = dict(os.environ)
dump_database_names = database_names_to_dump(
database, config, username, password, environment, dry_run
)
dump_database_names = database_names_to_dump(database, config, environment, dry_run)
if not dump_database_names:
if dry_run:
@@ -192,6 +201,8 @@ def dump_data_sources(
execute_dump_command(
renamed_database,
config,
username,
password,
dump_path,
(dump_name,),
environment,
@@ -204,6 +215,8 @@ def dump_data_sources(
execute_dump_command(
database,
config,
username,
password,
dump_path,
dump_database_names,
environment,
@@ -277,6 +290,7 @@ def restore_data_source_dump(
port = str(
connection_params['port'] or data_source.get('restore_port', data_source.get('port', ''))
)
tls = data_source.get('restore_tls', data_source.get('tls'))
username = borgmatic.hooks.credential.parse.resolve_credential(
(
connection_params['username']
@@ -295,20 +309,23 @@ def restore_data_source_dump(
mysql_restore_command = tuple(
shlex.quote(part) for part in shlex.split(data_source.get('mysql_command') or 'mysql')
)
extra_options, defaults_extra_filename = (
borgmatic.hooks.data_source.mariadb.parse_extra_options(data_source.get('restore_options'))
)
restore_command = (
mysql_restore_command
+ ('--batch',)
+ (
tuple(data_source['restore_options'].split(' '))
if 'restore_options' in data_source
else ()
+ borgmatic.hooks.data_source.mariadb.make_defaults_file_options(
username, password, defaults_extra_filename
)
+ extra_options
+ ('--batch',)
+ (('--host', hostname) if hostname else ())
+ (('--port', str(port)) if port else ())
+ (('--protocol', 'tcp') if hostname or port else ())
+ (('--user', username) if username else ())
+ (('--ssl',) if tls is True else ())
+ (('--skip-ssl',) if tls is False else ())
)
environment = dict(os.environ, **({'MYSQL_PWD': password} if password else {}))
environment = dict(os.environ)
logger.debug(f"Restoring MySQL database {data_source['name']}{dry_run_label}")
if dry_run:
@@ -159,6 +159,7 @@ def dump_data_sources(
for database_name in dump_database_names:
dump_format = database.get('format', None if database_name == 'all' else 'custom')
compression = database.get('compression')
default_dump_command = 'pg_dumpall' if database_name == 'all' else 'pg_dump'
dump_command = tuple(
shlex.quote(part)
@@ -199,6 +200,7 @@ def dump_data_sources(
)
+ (('--no-owner',) if database.get('no_owner', False) else ())
+ (('--format', shlex.quote(dump_format)) if dump_format else ())
+ (('--compress', shlex.quote(str(compression))) if compression is not None else ())
+ (('--file', shlex.quote(dump_filename)) if dump_format == 'directory' else ())
+ (
tuple(shlex.quote(option) for option in database['options'].split(' '))
+11 -2
View File
@@ -134,7 +134,16 @@ def get_all_dataset_mount_points(zfs_command):
)
)
return tuple(sorted(line.rstrip() for line in list_output.splitlines()))
return tuple(
sorted(
{
mount_point
for line in list_output.splitlines()
for mount_point in (line.rstrip(),)
if mount_point != 'none'
}
)
)
def snapshot_dataset(zfs_command, full_snapshot_name): # pragma: no cover
@@ -411,7 +420,7 @@ def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, d
continue
if not dry_run:
shutil.rmtree(snapshots_directory)
shutil.rmtree(snapshot_mount_path, ignore_errors=True)
# Destroy snapshots.
full_snapshot_names = get_all_snapshots(zfs_command)
+1 -1
View File
@@ -64,7 +64,7 @@ def get_handler(identifier):
def format_buffered_logs_for_payload(identifier):
'''
Get the handler previously added to the root logger, and slurp buffered logs out of it to
send to Healthchecks.
send to the monitoring service.
'''
try:
buffering_handler = get_handler(identifier)
+32 -11
View File
@@ -6,20 +6,36 @@ import platform
import requests
import borgmatic.hooks.credential.parse
import borgmatic.hooks.monitoring.logs
from borgmatic.hooks.monitoring import monitor
logger = logging.getLogger(__name__)
EVENTS_API_URL = 'https://events.pagerduty.com/v2/enqueue'
DEFAULT_LOGS_PAYLOAD_LIMIT_BYTES = 10000
HANDLER_IDENTIFIER = 'pagerduty'
def initialize_monitor(
integration_key, config, config_filename, monitoring_log_level, dry_run
): # pragma: no cover
def initialize_monitor(hook_config, config, config_filename, monitoring_log_level, dry_run):
'''
No initialization is necessary for this monitor.
Add a handler to the root logger that stores in memory the most recent logs emitted. That way,
we can send them all to PagerDuty upon a failure state. But skip this if the "send_logs" option
is false.
'''
pass
if hook_config.get('send_logs') is False:
return
ping_body_limit = max(
DEFAULT_LOGS_PAYLOAD_LIMIT_BYTES
- len(borgmatic.hooks.monitoring.logs.PAYLOAD_TRUNCATION_INDICATOR),
0,
)
borgmatic.hooks.monitoring.logs.add_handler(
borgmatic.hooks.monitoring.logs.Forgetful_buffering_handler(
HANDLER_IDENTIFIER, ping_body_limit, monitoring_log_level
)
)
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
@@ -37,9 +53,6 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
dry_run_label = ' (dry run; not actually sending)' if dry_run else ''
logger.info(f'Sending failure event to PagerDuty {dry_run_label}')
if dry_run:
return
try:
integration_key = borgmatic.hooks.credential.parse.resolve_credential(
hook_config.get('integration_key'), config
@@ -48,6 +61,10 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
logger.warning(f'PagerDuty credential error: {error}')
return
logs_payload = borgmatic.hooks.monitoring.logs.format_buffered_logs_for_payload(
HANDLER_IDENTIFIER
)
hostname = platform.node()
local_timestamp = datetime.datetime.now(datetime.timezone.utc).astimezone().isoformat()
payload = json.dumps(
@@ -66,11 +83,14 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
'hostname': hostname,
'configuration filename': config_filename,
'server time': local_timestamp,
'logs': logs_payload,
},
},
}
)
logger.debug(f'Using PagerDuty payload: {payload}')
if dry_run:
return
logging.getLogger('urllib3').setLevel(logging.ERROR)
try:
@@ -83,6 +103,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
def destroy_monitor(ping_url_or_uuid, config, monitoring_log_level, dry_run): # pragma: no cover
'''
No destruction is necessary for this monitor.
Remove the monitor handler that was added to the root logger. This prevents the handler from
getting reused by other instances of this monitor.
'''
pass
borgmatic.hooks.monitoring.logs.remove_handler(HANDLER_IDENTIFIER)
+1 -1
View File
@@ -37,7 +37,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
logging.getLogger('urllib3').setLevel(logging.ERROR)
try:
response = requests.get(f'{push_url}?{query}')
response = requests.get(f'{push_url}?{query}', verify=hook_config.get('verify_tls', True))
if not response.ok:
response.raise_for_status()
except requests.exceptions.RequestException as error:
+67 -29
View File
@@ -16,6 +16,42 @@ def initialize_monitor(
pass
def send_zabbix_request(server, headers, data):
'''
Given a Zabbix server URL, HTTP headers as a dict, and valid Zabbix JSON payload data as a dict,
send a request to the Zabbix server via API.
Return the response "result" value or None.
'''
logging.getLogger('urllib3').setLevel(logging.ERROR)
logger.debug(f'Sending a "{data["method"]}" request to the Zabbix server')
try:
response = requests.post(server, headers=headers, json=data)
if not response.ok:
response.raise_for_status()
except requests.exceptions.RequestException as error:
logger.warning(f'Zabbix error: {error}')
return None
try:
result = response.json().get('result')
error_message = result['data'][0]['error']
except requests.exceptions.JSONDecodeError:
logger.warning('Zabbix error: Cannot parse API response')
return None
except (TypeError, KeyError, IndexError):
return result
else:
logger.warning(f'Zabbix error: {error_message}')
return None
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
'''
Update the configured Zabbix item using either the itemid, or a host and key.
@@ -48,6 +84,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
)
except ValueError as error:
logger.warning(f'Zabbix credential error: {error}')
return
server = hook_config.get('server')
@@ -57,13 +94,9 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
value = state_config.get('value')
headers = {'Content-Type': 'application/json-rpc'}
logger.info(f'Updating Zabbix{dry_run_label}')
logger.info(f'Pinging Zabbix{dry_run_label}')
logger.debug(f'Using Zabbix URL: {server}')
if server is None:
logger.warning('Server missing for Zabbix')
return
# Determine the Zabbix method used to store the value: itemid or host/key
if itemid is not None:
logger.info(f'Updating {itemid} on Zabbix')
@@ -74,8 +107,8 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
'id': 1,
}
elif (host and key) is not None:
logger.info(f'Updating Host:{host} and Key:{key} on Zabbix')
elif host is not None and key is not None:
logger.info(f'Updating Host: "{host}" and Key: "{key}" on Zabbix')
data = {
'jsonrpc': '2.0',
'method': 'history.push',
@@ -85,58 +118,63 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
elif host is not None:
logger.warning('Key missing for Zabbix')
return
return
elif key is not None:
logger.warning('Host missing for Zabbix')
return
else:
logger.warning('No Zabbix itemid or host/key provided')
return
# Determine the authentication method: API key or username/password
if api_key is not None:
logger.info('Using API key auth for Zabbix')
headers['Authorization'] = 'Bearer ' + api_key
elif (username and password) is not None:
logger.info('Using user/pass auth with user {username} for Zabbix')
auth_data = {
headers['Authorization'] = f'Bearer {api_key}'
elif username is not None and password is not None:
logger.info(f'Using user/pass auth with user {username} for Zabbix')
login_data = {
'jsonrpc': '2.0',
'method': 'user.login',
'params': {'username': username, 'password': password},
'id': 1,
}
if not dry_run:
logging.getLogger('urllib3').setLevel(logging.ERROR)
try:
response = requests.post(server, headers=headers, json=auth_data)
data['auth'] = response.json().get('result')
if not response.ok:
response.raise_for_status()
except requests.exceptions.RequestException as error:
logger.warning(f'Zabbix error: {error}')
result = send_zabbix_request(server, headers, login_data)
if not result:
return
headers['Authorization'] = f'Bearer {result}'
elif username is not None:
logger.warning('Password missing for Zabbix authentication')
return
return
elif password is not None:
logger.warning('Username missing for Zabbix authentication')
return
else:
logger.warning('Authentication data missing for Zabbix')
return
if not dry_run:
logging.getLogger('urllib3').setLevel(logging.ERROR)
try:
response = requests.post(server, headers=headers, json=data)
if not response.ok:
response.raise_for_status()
except requests.exceptions.RequestException as error:
logger.warning(f'Zabbix error: {error}')
send_zabbix_request(server, headers, data)
if username is not None and password is not None:
logout_data = {
'jsonrpc': '2.0',
'method': 'user.logout',
'params': [],
'id': 1,
}
if not dry_run:
send_zabbix_request(server, headers, logout_data)
def destroy_monitor(ping_url_or_uuid, config, monitoring_log_level, dry_run): # pragma: no cover
+3
View File
@@ -24,6 +24,9 @@ def handle_signal(signal_number, frame):
logger.critical('Exiting due to TERM signal')
sys.exit(EXIT_CODE_FROM_SIGNAL + signal.SIGTERM)
elif signal_number == signal.SIGINT:
# Borg doesn't always exit on a SIGINT, so give it a little encouragement.
os.killpg(os.getpgrp(), signal.SIGTERM)
raise KeyboardInterrupt()
+2 -3
View File
@@ -26,8 +26,7 @@ def list_merged_pulls(url):
def list_contributing_issues(url):
# labels = bug, design finalized, etc.
response = requests.get(f'{url}?labels=19,20,22,23,32,52,53,54', headers={'Accept': 'application/json', 'Content-Type': 'application/json'})
response = requests.get(url, headers={'Accept': 'application/json', 'Content-Type': 'application/json'})
if not response.ok:
response.raise_for_status()
@@ -39,7 +38,7 @@ PULLS_API_ENDPOINT_URLS = (
'https://projects.torsion.org/api/v1/repos/borgmatic-collective/borgmatic/pulls',
'https://api.github.com/repos/borgmatic-collective/borgmatic/pulls',
)
ISSUES_API_ENDPOINT_URL = 'https://projects.torsion.org/api/v1/repos/borgmatic-collective/borgmatic/issues'
ISSUES_API_ENDPOINT_URL = 'https://projects.torsion.org/api/v1/repos/borgmatic-collective/borgmatic/issues?state=all'
RECENT_CONTRIBUTORS_CUTOFF_DAYS = 365
@@ -7,12 +7,13 @@ eleventyNavigation:
---
## Preparation and cleanup hooks
If you find yourself performing preparation tasks before your backup runs, or
cleanup work afterwards, borgmatic hooks may be of interest. Hooks are shell
commands that borgmatic executes for you at various points as it runs, and
they're configured in the `hooks` section of your configuration file. But if
you're looking to backup a database, it's probably easier to use the [database
backup
If you find yourself performing preparation tasks before your backup runs or
cleanup work afterwards, borgmatic command hooks may be of interest. These are
custom shell commands you can configure borgmatic to execute at various points
as it runs.
But if you're looking to backup a database, it's probably easier to use the
[database backup
feature](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/)
instead.
+2 -4
View File
@@ -309,10 +309,8 @@ hooks:
### External passwords
If you don't want to keep your database passwords in your borgmatic
configuration file, you can instead pass them in via [environment
variables](https://torsion.org/borgmatic/docs/how-to/provide-your-passwords/)
or command-line [configuration
overrides](https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/#configuration-overrides).
configuration file, you can instead pass them in [from external credential
sources](https://torsion.org/borgmatic/docs/how-to/provide-your-passwords/).
### Configuration backups
+37 -6
View File
@@ -292,6 +292,27 @@ If you have any issues with the integration, [please contact
us](https://torsion.org/borgmatic/#support-and-contributing).
### Sending logs
<span class="minilink minilink-addedin">New in version 1.9.14</span> borgmatic
logs are included in the payload data sent to PagerDuty. This means that
(truncated) borgmatic logs, including error messages, show up in the PagerDuty
incident UI and corresponding notification emails.
You can customize the verbosity of the logs that are sent with borgmatic's
`--monitoring-verbosity` flag. The `--list` and `--stats` flags may also be of
use. See `borgmatic create --help` for more information.
If you don't want any logs sent, you can disable this feature by setting
`send_logs` to `false`:
```yaml
pagerduty:
integration_key: a177cad45bd374409f78906a810a3074
send_logs: false
```
## Pushover hook
<span class="minilink minilink-addedin">New in version 1.9.2</span>
@@ -710,8 +731,10 @@ zabbix:
- fail
```
This hook requires the Zabbix server be running version 7.0. ([Support for Zabbix
7.2+](https://projects.torsion.org/borgmatic-collective/borgmatic/issues/1003) is planned.)
This hook requires the Zabbix server be running version 7.0.
<span class="minilink minilink-addedin">New in version 1.9.3</span> Zabbix 7.2+
is supported as well.
### Authentication methods
@@ -722,11 +745,19 @@ Authentication can be accomplished via `api_key` or both `username` and
### Items
The item to be updated can be chosen by either declaring the `itemid` or both
`host` and `key`. If all three are declared, only `itemid` is used.
borgmatic writes its monitoring updates to a particular Zabbix item, which
you'll need to create in advance. In the Zabbix web UI, [make a new item with a
Type of "Zabbix
trapper"](https://www.zabbix.com/documentation/current/en/manual/config/items/itemtypes/trapper)
and a named Key. The "Type of information" for the item should be "Text", and
"History" designates how much data you want to retain.
Keep in mind that `host` is referring to the "Host name" on the Zabbix server
and not the "Visual name".
When configuring borgmatic with this item to be updated, you can either declare
the `itemid` or both `host` and `key`. If all three are declared, only `itemid`
is used.
Keep in mind that `host` refers to the "Host name" on the Zabbix server and not
the "Visual name".
## Scripting borgmatic
+11 -2
View File
@@ -148,8 +148,9 @@ feedback](https://torsion.org/borgmatic/#issues) you have on this feature.
#### Subvolume discovery
For any subvolume you'd like backed up, add its path to borgmatic's
`source_directories` option.
For any read-write subvolume you'd like backed up, add its path to borgmatic's
`source_directories` option. Btrfs does not support snapshotting read-only
subvolumes.
<span class="minilink minilink-addedin">New in version 1.9.6</span> Or include
the mount point as a root pattern with borgmatic's `patterns` or `patterns_from`
@@ -205,6 +206,14 @@ Volume Manager) and sending those snapshots to Borg for backup. LVM isn't
itself a filesystem, but it can take snapshots at the layer right below your
filesystem.
Note that, due to Borg being a file-level backup, this feature is really only
suitable for filesystems, not whole disk or raw images containing multiple
filesystems (for example, if you're using a LVM volume to run a Windows
KVM that contains an MBR, partitions, etc.).
In those cases, you can omit the `lvm:` option and use Borg's own support for
[image backup](https://borgbackup.readthedocs.io/en/stable/deployment/image-backup.html).
To use this feature, first you need one or more mounted LVM logical volumes.
Then, enable LVM within borgmatic by adding the following line to your
configuration file:
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "borgmatic"
version = "1.9.11"
version = "1.9.14"
authors = [
{ name="Dan Helfman", email="witten@torsion.org" },
]
+15 -2
View File
@@ -24,7 +24,14 @@ def parse_arguments(*unparsed_arguments):
delete_parser = subvolume_subparser.add_parser('delete')
delete_parser.add_argument('snapshot_path')
return global_parser.parse_args(unparsed_arguments)
property_parser = action_parsers.add_parser('property')
property_subparser = property_parser.add_subparsers(dest='subaction')
get_parser = property_subparser.add_parser('get')
get_parser.add_argument('-t', dest='type')
get_parser.add_argument('subvolume_path')
get_parser.add_argument('property_name')
return (global_parser, global_parser.parse_args(unparsed_arguments))
BUILTIN_SUBVOLUME_LIST_LINES = (
@@ -60,9 +67,13 @@ def print_subvolume_list(arguments, snapshot_paths):
def main():
arguments = parse_arguments(*sys.argv[1:])
(global_parser, arguments) = parse_arguments(*sys.argv[1:])
snapshot_paths = load_snapshots()
if not hasattr(arguments, 'subaction'):
global_parser.print_help()
sys.exit(1)
if arguments.subaction == 'list':
print_subvolume_list(arguments, snapshot_paths)
elif arguments.subaction == 'snapshot':
@@ -84,6 +95,8 @@ def main():
if snapshot_path.endswith('/' + arguments.snapshot_path)
]
save_snapshots(snapshot_paths)
elif arguments.action == 'property' and arguments.subaction == 'get':
print(f'{arguments.property_name}=false')
if __name__ == '__main__':
+15 -6
View File
@@ -225,15 +225,24 @@ def test_expand_patterns_considers_none_as_no_patterns():
assert module.expand_patterns(None) == ()
def test_expand_patterns_only_considers_root_patterns():
flexmock(module).should_receive('expand_directory').with_args('~/foo', None).and_return(
['/root/foo']
def test_expand_patterns_expands_tildes_and_globs_in_root_patterns():
flexmock(module.os.path).should_receive('expanduser').never()
flexmock(module).should_receive('expand_directory').and_return(
['/root/foo/one', '/root/foo/two']
)
flexmock(module).should_receive('expand_directory').with_args('bar*', None).never()
paths = module.expand_patterns((Pattern('~/foo'), Pattern('bar*', Pattern_type.INCLUDE)))
paths = module.expand_patterns((Pattern('~/foo/*'),))
assert paths == (Pattern('/root/foo'), Pattern('bar*', Pattern_type.INCLUDE))
assert paths == (Pattern('/root/foo/one'), Pattern('/root/foo/two'))
def test_expand_patterns_expands_only_tildes_in_non_root_patterns():
flexmock(module).should_receive('expand_directory').never()
flexmock(module.os.path).should_receive('expanduser').and_return('/root/bar/*')
paths = module.expand_patterns((Pattern('~/bar/*', Pattern_type.INCLUDE),))
assert paths == (Pattern('/root/bar/*', Pattern_type.INCLUDE),)
def test_device_map_patterns_gives_device_id_per_path():
@@ -49,8 +49,61 @@ def test_get_subvolume_mount_points_with_findmnt_json_missing_filesystems_errors
module.get_subvolume_mount_points('findmnt')
def test_get_subvolume_property_with_invalid_btrfs_output_errors():
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).and_return('invalid')
with pytest.raises(ValueError):
module.get_subvolume_property('btrfs', '/foo', 'ro')
def test_get_subvolume_property_with_true_output_returns_true_bool():
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).and_return('ro=true')
assert module.get_subvolume_property('btrfs', '/foo', 'ro') is True
def test_get_subvolume_property_with_false_output_returns_false_bool():
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).and_return('ro=false')
assert module.get_subvolume_property('btrfs', '/foo', 'ro') is False
def test_get_subvolume_property_passes_through_general_value():
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).and_return('thing=value')
assert module.get_subvolume_property('btrfs', '/foo', 'thing') == 'value'
def test_omit_read_only_subvolume_mount_points_filters_out_read_only():
flexmock(module).should_receive('get_subvolume_property').with_args(
'btrfs', '/foo', 'ro'
).and_return(False)
flexmock(module).should_receive('get_subvolume_property').with_args(
'btrfs', '/bar', 'ro'
).and_return(True)
flexmock(module).should_receive('get_subvolume_property').with_args(
'btrfs', '/baz', 'ro'
).and_return(False)
assert module.omit_read_only_subvolume_mount_points('btrfs', ('/foo', '/bar', '/baz')) == (
'/foo',
'/baz',
)
def test_get_subvolumes_collects_subvolumes_matching_patterns():
flexmock(module).should_receive('get_subvolume_mount_points').and_return(('/mnt1', '/mnt2'))
flexmock(module).should_receive('omit_read_only_subvolume_mount_points').and_return(
('/mnt1', '/mnt2')
)
contained_pattern = Pattern(
'/mnt1',
@@ -76,6 +129,9 @@ def test_get_subvolumes_collects_subvolumes_matching_patterns():
def test_get_subvolumes_skips_non_root_patterns():
flexmock(module).should_receive('get_subvolume_mount_points').and_return(('/mnt1', '/mnt2'))
flexmock(module).should_receive('omit_read_only_subvolume_mount_points').and_return(
('/mnt1', '/mnt2')
)
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_patterns'
@@ -107,6 +163,9 @@ def test_get_subvolumes_skips_non_root_patterns():
def test_get_subvolumes_skips_non_config_patterns():
flexmock(module).should_receive('get_subvolume_mount_points').and_return(('/mnt1', '/mnt2'))
flexmock(module).should_receive('omit_read_only_subvolume_mount_points').and_return(
('/mnt1', '/mnt2')
)
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_patterns'
@@ -138,6 +197,9 @@ def test_get_subvolumes_skips_non_config_patterns():
def test_get_subvolumes_without_patterns_collects_all_subvolumes():
flexmock(module).should_receive('get_subvolume_mount_points').and_return(('/mnt1', '/mnt2'))
flexmock(module).should_receive('omit_read_only_subvolume_mount_points').and_return(
('/mnt1', '/mnt2')
)
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_patterns'
+500 -29
View File
@@ -6,10 +6,124 @@ from flexmock import flexmock
from borgmatic.hooks.data_source import mariadb as module
def test_parse_extra_options_passes_through_empty_options():
assert module.parse_extra_options('') == ((), None)
def test_parse_extra_options_with_defaults_extra_file_removes_and_and_parses_out_filename():
assert module.parse_extra_options('--defaults-extra-file=extra.cnf --skip-ssl') == (
('--skip-ssl',),
'extra.cnf',
)
def test_parse_extra_options_without_defaults_extra_file_passes_through_options():
assert module.parse_extra_options('--skip-ssl --and=stuff') == (
('--skip-ssl', '--and=stuff'),
None,
)
def test_make_defaults_file_pipe_without_username_or_password_bails():
flexmock(module.os).should_receive('pipe').never()
assert module.make_defaults_file_options(username=None, password=None) == ()
def test_make_defaults_file_option_with_username_and_password_writes_them_to_file_descriptor():
read_descriptor = 99
write_descriptor = flexmock()
flexmock(module.os).should_receive('pipe').and_return(read_descriptor, write_descriptor)
flexmock(module.os).should_receive('write').with_args(
write_descriptor, b'[client]\nuser=root\npassword="trustsome1"'
).once()
flexmock(module.os).should_receive('close')
flexmock(module.os).should_receive('set_inheritable')
assert module.make_defaults_file_options(username='root', password='trustsome1') == (
'--defaults-extra-file=/dev/fd/99',
)
def test_make_defaults_file_escapes_password_containing_backslash():
read_descriptor = 99
write_descriptor = flexmock()
flexmock(module.os).should_receive('pipe').and_return(read_descriptor, write_descriptor)
flexmock(module.os).should_receive('write').with_args(
write_descriptor, b'[client]\nuser=root\n' + br'password="trust\\nsome1"'
).once()
flexmock(module.os).should_receive('close')
flexmock(module.os).should_receive('set_inheritable')
assert module.make_defaults_file_options(username='root', password=r'trust\nsome1') == (
'--defaults-extra-file=/dev/fd/99',
)
def test_make_defaults_file_pipe_with_only_username_writes_it_to_file_descriptor():
read_descriptor = 99
write_descriptor = flexmock()
flexmock(module.os).should_receive('pipe').and_return(read_descriptor, write_descriptor)
flexmock(module.os).should_receive('write').with_args(
write_descriptor, b'[client]\nuser=root'
).once()
flexmock(module.os).should_receive('close')
flexmock(module.os).should_receive('set_inheritable')
assert module.make_defaults_file_options(username='root', password=None) == (
'--defaults-extra-file=/dev/fd/99',
)
def test_make_defaults_file_pipe_with_only_password_writes_it_to_file_descriptor():
read_descriptor = 99
write_descriptor = flexmock()
flexmock(module.os).should_receive('pipe').and_return(read_descriptor, write_descriptor)
flexmock(module.os).should_receive('write').with_args(
write_descriptor, b'[client]\npassword="trustsome1"'
).once()
flexmock(module.os).should_receive('close')
flexmock(module.os).should_receive('set_inheritable')
assert module.make_defaults_file_options(username=None, password='trustsome1') == (
'--defaults-extra-file=/dev/fd/99',
)
def test_make_defaults_file_option_with_defaults_extra_filename_includes_it_in_file_descriptor():
read_descriptor = 99
write_descriptor = flexmock()
flexmock(module.os).should_receive('pipe').and_return(read_descriptor, write_descriptor)
flexmock(module.os).should_receive('write').with_args(
write_descriptor, b'!include extra.cnf\n[client]\nuser=root\npassword="trustsome1"'
).once()
flexmock(module.os).should_receive('close')
flexmock(module.os).should_receive('set_inheritable')
assert module.make_defaults_file_options(
username='root', password='trustsome1', defaults_extra_filename='extra.cnf'
) == ('--defaults-extra-file=/dev/fd/99',)
def test_make_defaults_file_option_with_only_defaults_extra_filename_uses_it_instead_of_file_descriptor():
flexmock(module.os).should_receive('pipe').never()
assert module.make_defaults_file_options(
username=None, password=None, defaults_extra_filename='extra.cnf'
) == ('--defaults-extra-file=extra.cnf',)
def test_database_names_to_dump_passes_through_name():
environment = flexmock()
names = module.database_names_to_dump({'name': 'foo'}, {}, environment, dry_run=False)
names = module.database_names_to_dump(
{'name': 'foo'}, {}, 'root', 'trustsome1', environment, dry_run=False
)
assert names == ('foo',)
@@ -18,7 +132,9 @@ def test_database_names_to_dump_bails_for_dry_run():
environment = flexmock()
flexmock(module).should_receive('execute_command_and_capture_output').never()
names = module.database_names_to_dump({'name': 'all'}, {}, environment, dry_run=True)
names = module.database_names_to_dump(
{'name': 'all'}, {}, 'root', 'trustsome1', environment, dry_run=True
)
assert names == ()
@@ -28,12 +144,83 @@ def test_database_names_to_dump_queries_mariadb_for_database_names():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root', 'trustsome1', None
).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('mariadb', '--skip-column-names', '--batch', '--execute', 'show schemas'),
(
'mariadb',
'--defaults-extra-file=/dev/fd/99',
'--skip-column-names',
'--batch',
'--execute',
'show schemas',
),
environment=environment,
).and_return('foo\nbar\nmysql\n').once()
names = module.database_names_to_dump({'name': 'all'}, {}, environment, dry_run=False)
names = module.database_names_to_dump(
{'name': 'all'}, {}, 'root', 'trustsome1', environment, dry_run=False
)
assert names == ('foo', 'bar')
def test_database_names_to_dump_runs_mariadb_with_tls():
environment = flexmock()
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root', 'trustsome1', None
).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
(
'mariadb',
'--defaults-extra-file=/dev/fd/99',
'--ssl',
'--skip-column-names',
'--batch',
'--execute',
'show schemas',
),
environment=environment,
).and_return('foo\nbar\nmysql\n').once()
names = module.database_names_to_dump(
{'name': 'all', 'tls': True}, {}, 'root', 'trustsome1', environment, dry_run=False
)
assert names == ('foo', 'bar')
def test_database_names_to_dump_runs_mariadb_without_tls():
environment = flexmock()
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root', 'trustsome1', None
).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
(
'mariadb',
'--defaults-extra-file=/dev/fd/99',
'--skip-ssl',
'--skip-column-names',
'--batch',
'--execute',
'show schemas',
),
environment=environment,
).and_return('foo\nbar\nmysql\n').once()
names = module.database_names_to_dump(
{'name': 'all', 'tls': False}, {}, 'root', 'trustsome1', environment, dry_run=False
)
assert names == ('foo', 'bar')
@@ -53,6 +240,9 @@ def test_dump_data_sources_dumps_each_database():
databases = [{'name': 'foo'}, {'name': 'bar'}]
processes = [flexmock(), flexmock()]
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).and_return(None)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
@@ -65,6 +255,8 @@ def test_dump_data_sources_dumps_each_database():
flexmock(module).should_receive('execute_dump_command').with_args(
database={'name': name},
config={},
username=None,
password=None,
dump_path=object,
database_names=(name,),
environment={'USER': 'root'},
@@ -89,10 +281,10 @@ def test_dump_data_sources_dumps_with_password():
database = {'name': 'foo', 'username': 'root', 'password': 'trustsome1'}
process = flexmock()
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
('bar',)
)
@@ -100,9 +292,11 @@ def test_dump_data_sources_dumps_with_password():
flexmock(module).should_receive('execute_dump_command').with_args(
database=database,
config={},
username='root',
password='trustsome1',
dump_path=object,
database_names=('foo',),
environment={'USER': 'root', 'MYSQL_PWD': 'trustsome1'},
environment={'USER': 'root'},
dry_run=object,
dry_run_label=object,
).and_return(process).once()
@@ -121,14 +315,16 @@ def test_dump_data_sources_dumps_all_databases_at_once():
databases = [{'name': 'all'}]
process = flexmock()
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
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={},
username=None,
password=None,
dump_path=object,
database_names=('foo', 'bar'),
environment={'USER': 'root'},
@@ -150,16 +346,18 @@ def test_dump_data_sources_dumps_all_databases_separately_when_format_configured
databases = [{'name': 'all', 'format': 'sql'}]
processes = [flexmock(), flexmock()]
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
).and_return(None)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
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={},
username=None,
password=None,
dump_path=object,
database_names=(name,),
environment={'USER': 'root'},
@@ -181,11 +379,18 @@ def test_dump_data_sources_dumps_all_databases_separately_when_format_configured
def test_database_names_to_dump_runs_mariadb_with_list_options():
database = {'name': 'all', 'list_options': '--defaults-extra-file=mariadb.cnf'}
database = {'name': 'all', 'list_options': '--defaults-extra-file=mariadb.cnf --skip-ssl'}
flexmock(module).should_receive('parse_extra_options').and_return(
('--skip-ssl',), 'mariadb.cnf'
)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root', 'trustsome1', 'mariadb.cnf'
).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
(
'mariadb',
'--defaults-extra-file=mariadb.cnf',
'--defaults-extra-file=/dev/fd/99',
'--skip-ssl',
'--skip-column-names',
'--batch',
'--execute',
@@ -194,20 +399,30 @@ def test_database_names_to_dump_runs_mariadb_with_list_options():
environment=None,
).and_return(('foo\nbar')).once()
assert module.database_names_to_dump(database, {}, None, '') == ('foo', 'bar')
assert module.database_names_to_dump(database, {}, 'root', 'trustsome1', None, '') == (
'foo',
'bar',
)
def test_database_names_to_dump_runs_non_default_mariadb_with_list_options():
database = {
'name': 'all',
'list_options': '--defaults-extra-file=mariadb.cnf',
'list_options': '--defaults-extra-file=mariadb.cnf --skip-ssl',
'mariadb_command': 'custom_mariadb',
}
flexmock(module).should_receive('parse_extra_options').and_return(
('--skip-ssl',), 'mariadb.cnf'
)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root', 'trustsome1', 'mariadb.cnf'
).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
environment=None,
full_command=(
'custom_mariadb', # Custom MariaDB command
'--defaults-extra-file=mariadb.cnf',
'--defaults-extra-file=/dev/fd/99',
'--skip-ssl',
'--skip-column-names',
'--batch',
'--execute',
@@ -215,7 +430,10 @@ 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, {}, 'root', 'trustsome1', None, '') == (
'foo',
'bar',
)
def test_execute_dump_command_runs_mariadb_dump():
@@ -225,11 +443,16 @@ def test_execute_dump_command_runs_mariadb_dump():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root', 'trustsome1', None
).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
(
'mariadb-dump',
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--databases',
'foo',
@@ -244,6 +467,8 @@ def test_execute_dump_command_runs_mariadb_dump():
module.execute_dump_command(
database={'name': 'foo'},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment=None,
@@ -261,11 +486,16 @@ def test_execute_dump_command_runs_mariadb_dump_without_add_drop_database():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root', 'trustsome1', None
).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
(
'mariadb-dump',
'--defaults-extra-file=/dev/fd/99',
'--databases',
'foo',
'--result-file',
@@ -279,6 +509,8 @@ def test_execute_dump_command_runs_mariadb_dump_without_add_drop_database():
module.execute_dump_command(
database={'name': 'foo', 'add_drop_database': False},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment=None,
@@ -296,11 +528,16 @@ def test_execute_dump_command_runs_mariadb_dump_with_hostname_and_port():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root', 'trustsome1', None
).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
(
'mariadb-dump',
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--host',
'database.example.org',
@@ -321,6 +558,96 @@ def test_execute_dump_command_runs_mariadb_dump_with_hostname_and_port():
module.execute_dump_command(
database={'name': 'foo', 'hostname': 'database.example.org', 'port': 5433},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment=None,
dry_run=False,
dry_run_label='',
)
== process
)
def test_execute_dump_command_runs_mariadb_dump_with_tls():
process = flexmock()
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root', 'trustsome1', None
).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
(
'mariadb-dump',
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--ssl',
'--databases',
'foo',
'--result-file',
'dump',
),
environment=None,
run_to_completion=False,
).and_return(process).once()
assert (
module.execute_dump_command(
database={'name': 'foo', 'tls': True},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment=None,
dry_run=False,
dry_run_label='',
)
== process
)
def test_execute_dump_command_runs_mariadb_dump_without_tls():
process = flexmock()
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root', 'trustsome1', None
).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
(
'mariadb-dump',
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--skip-ssl',
'--databases',
'foo',
'--result-file',
'dump',
),
environment=None,
run_to_completion=False,
).and_return(process).once()
assert (
module.execute_dump_command(
database={'name': 'foo', 'tls': False},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment=None,
@@ -338,20 +665,23 @@ def test_execute_dump_command_runs_mariadb_dump_with_username_and_password():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root', 'trustsome1', None
).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
(
'mariadb-dump',
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--user',
'root',
'--databases',
'foo',
'--result-file',
'dump',
),
environment={'MYSQL_PWD': 'trustsome1'},
environment={},
run_to_completion=False,
).and_return(process).once()
@@ -359,9 +689,11 @@ def test_execute_dump_command_runs_mariadb_dump_with_username_and_password():
module.execute_dump_command(
database={'name': 'foo', 'username': 'root', 'password': 'trustsome1'},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment={'MYSQL_PWD': 'trustsome1'},
environment={},
dry_run=False,
dry_run_label='',
)
@@ -376,11 +708,16 @@ def test_execute_dump_command_runs_mariadb_dump_with_options():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return(('--stuff=such',), None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root', 'trustsome1', None
).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
(
'mariadb-dump',
'--defaults-extra-file=/dev/fd/99',
'--stuff=such',
'--add-drop-database',
'--databases',
@@ -396,6 +733,8 @@ def test_execute_dump_command_runs_mariadb_dump_with_options():
module.execute_dump_command(
database={'name': 'foo', 'options': '--stuff=such'},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment=None,
@@ -413,11 +752,16 @@ def test_execute_dump_command_runs_non_default_mariadb_dump_with_options():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return(('--stuff=such',), None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root', 'trustsome1', None
).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
(
'custom_mariadb_dump', # Custom MariaDB dump command
'--defaults-extra-file=/dev/fd/99',
'--stuff=such',
'--add-drop-database',
'--databases',
@@ -437,6 +781,8 @@ def test_execute_dump_command_runs_non_default_mariadb_dump_with_options():
'options': '--stuff=such',
}, # Custom MariaDB dump command specified
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment=None,
@@ -450,6 +796,10 @@ def test_execute_dump_command_runs_non_default_mariadb_dump_with_options():
def test_execute_dump_command_with_duplicate_dump_skips_mariadb_dump():
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root', 'trustsome1', None
).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
flexmock(module).should_receive('execute_command').never()
@@ -457,6 +807,8 @@ def test_execute_dump_command_with_duplicate_dump_skips_mariadb_dump():
module.execute_dump_command(
database={'name': 'foo'},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment=None,
@@ -473,6 +825,10 @@ def test_execute_dump_command_with_dry_run_skips_mariadb_dump():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root', 'trustsome1', None
).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').never()
@@ -481,6 +837,8 @@ def test_execute_dump_command_with_dry_run_skips_mariadb_dump():
module.execute_dump_command(
database={'name': 'foo'},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment=None,
@@ -546,6 +904,10 @@ def test_restore_data_source_dump_runs_mariadb_to_restore():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
None, None, None
).and_return(())
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
('mariadb', '--batch'),
@@ -578,9 +940,13 @@ def test_restore_data_source_dump_runs_mariadb_with_options():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return(('--harder',), None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
None, None, None
).and_return(())
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
('mariadb', '--batch', '--harder'),
('mariadb', '--harder', '--batch'),
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
@@ -612,9 +978,13 @@ 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, config: value)
flexmock(module).should_receive('parse_extra_options').and_return(('--harder',), None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
None, None, None
).and_return(())
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
('custom_mariadb', '--batch', '--harder'),
('custom_mariadb', '--harder', '--batch'),
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
@@ -644,6 +1014,10 @@ 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, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
None, None, None
).and_return(())
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
@@ -678,6 +1052,86 @@ def test_restore_data_source_dump_runs_mariadb_with_hostname_and_port():
)
def test_restore_data_source_dump_runs_mariadb_with_tls():
hook_config = [{'name': 'foo', 'tls': True}]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
None, None, None
).and_return(())
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mariadb',
'--batch',
'--ssl',
),
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
environment={'USER': 'root'},
).once()
module.restore_data_source_dump(
hook_config,
{},
data_source=hook_config[0],
dry_run=False,
extract_process=extract_process,
connection_params={
'hostname': None,
'port': None,
'username': None,
'password': None,
},
borgmatic_runtime_directory='/run/borgmatic',
)
def test_restore_data_source_dump_runs_mariadb_without_tls():
hook_config = [{'name': 'foo', 'tls': False}]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
None, None, None
).and_return(())
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mariadb',
'--batch',
'--skip-ssl',
),
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
environment={'USER': 'root'},
).once()
module.restore_data_source_dump(
hook_config,
{},
data_source=hook_config[0],
dry_run=False,
extract_process=extract_process,
connection_params={
'hostname': None,
'port': None,
'username': None,
'password': None,
},
borgmatic_runtime_directory='/run/borgmatic',
)
def test_restore_data_source_dump_runs_mariadb_with_username_and_password():
hook_config = [{'name': 'foo', 'username': 'root', 'password': 'trustsome1'}]
extract_process = flexmock(stdout=flexmock())
@@ -685,13 +1139,17 @@ 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, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root', 'trustsome1', None
).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
('mariadb', '--batch', '--user', 'root'),
('mariadb', '--defaults-extra-file=/dev/fd/99', '--batch'),
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
environment={'USER': 'root', 'MYSQL_PWD': 'trustsome1'},
environment={'USER': 'root'},
).once()
module.restore_data_source_dump(
@@ -727,10 +1185,15 @@ 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, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'cliusername', 'clipassword', None
).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mariadb',
'--defaults-extra-file=/dev/fd/99',
'--batch',
'--host',
'clihost',
@@ -738,13 +1201,11 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
'cliport',
'--protocol',
'tcp',
'--user',
'cliusername',
),
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
environment={'USER': 'root', 'MYSQL_PWD': 'clipassword'},
environment={'USER': 'root'},
).once()
module.restore_data_source_dump(
@@ -771,10 +1232,12 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
'password': 'trustsome1',
'hostname': 'dbhost',
'port': 'dbport',
'tls': True,
'restore_username': 'restoreuser',
'restore_password': 'restorepass',
'restore_hostname': 'restorehost',
'restore_port': 'restoreport',
'restore_tls': False,
}
]
extract_process = flexmock(stdout=flexmock())
@@ -782,10 +1245,15 @@ 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, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'restoreuser', 'restorepass', None
).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mariadb',
'--defaults-extra-file=/dev/fd/99',
'--batch',
'--host',
'restorehost',
@@ -793,13 +1261,12 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
'restoreport',
'--protocol',
'tcp',
'--user',
'restoreuser',
'--skip-ssl',
),
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
environment={'USER': 'root', 'MYSQL_PWD': 'restorepass'},
environment={'USER': 'root'},
).once()
module.restore_data_source_dump(
@@ -824,6 +1291,10 @@ 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, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
None, None, None
).and_return(())
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').never()
+36 -8
View File
@@ -127,6 +127,9 @@ 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, config: value)
flexmock(module).should_receive('make_password_config_file').with_args('trustsome1').and_return(
'/dev/fd/99'
)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -134,8 +137,8 @@ def test_dump_data_sources_runs_mongodump_with_username_and_password():
'mongodump',
'--username',
'mongo',
'--password',
'trustsome1',
'--config',
'/dev/fd/99',
'--authenticationDatabase',
'admin',
'--db',
@@ -243,6 +246,22 @@ def test_dump_data_sources_runs_mongodumpall_for_all_databases():
) == [process]
def test_make_password_config_file_writes_password_to_pipe():
read_file_descriptor = 99
write_file_descriptor = flexmock()
flexmock(module.os).should_receive('pipe').and_return(
(read_file_descriptor, write_file_descriptor)
)
flexmock(module.os).should_receive('write').with_args(
write_file_descriptor, b'password: trustsome1'
).once()
flexmock(module.os).should_receive('close')
flexmock(module.os).should_receive('set_inheritable')
assert module.make_password_config_file('trustsome1') == '/dev/fd/99'
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(
@@ -345,6 +364,9 @@ def test_restore_data_source_dump_runs_mongorestore_with_username_and_password()
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module).should_receive('make_password_config_file').with_args('trustsome1').and_return(
'/dev/fd/99'
)
flexmock(module).should_receive('execute_command_with_processes').with_args(
[
'mongorestore',
@@ -352,8 +374,8 @@ def test_restore_data_source_dump_runs_mongorestore_with_username_and_password()
'--drop',
'--username',
'mongo',
'--password',
'trustsome1',
'--config',
'/dev/fd/99',
'--authenticationDatabase',
'admin',
],
@@ -399,6 +421,9 @@ 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, config: value)
flexmock(module).should_receive('make_password_config_file').with_args(
'clipassword'
).and_return('/dev/fd/99')
flexmock(module).should_receive('execute_command_with_processes').with_args(
[
'mongorestore',
@@ -410,8 +435,8 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
'cliport',
'--username',
'cliusername',
'--password',
'clipassword',
'--config',
'/dev/fd/99',
'--authenticationDatabase',
'admin',
],
@@ -457,6 +482,9 @@ 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, config: value)
flexmock(module).should_receive('make_password_config_file').with_args(
'restorepass'
).and_return('/dev/fd/99')
flexmock(module).should_receive('execute_command_with_processes').with_args(
[
'mongorestore',
@@ -468,8 +496,8 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
'restoreport',
'--username',
'restoreuser',
'--password',
'restorepass',
'--config',
'/dev/fd/99',
'--authenticationDatabase',
'admin',
],
+441 -26
View File
@@ -9,7 +9,9 @@ from borgmatic.hooks.data_source import mysql as module
def test_database_names_to_dump_passes_through_name():
environment = flexmock()
names = module.database_names_to_dump({'name': 'foo'}, {}, environment, dry_run=False)
names = module.database_names_to_dump(
{'name': 'foo'}, {}, 'root', 'trustsome1', environment, dry_run=False
)
assert names == ('foo',)
@@ -21,7 +23,9 @@ def test_database_names_to_dump_bails_for_dry_run():
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_and_capture_output').never()
names = module.database_names_to_dump({'name': 'all'}, {}, environment, dry_run=True)
names = module.database_names_to_dump(
{'name': 'all'}, {}, 'root', 'trustsome1', environment, dry_run=True
)
assert names == ()
@@ -31,12 +35,89 @@ def test_database_names_to_dump_queries_mysql_for_database_names():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return((), None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('mysql', '--skip-column-names', '--batch', '--execute', 'show schemas'),
(
'mysql',
'--defaults-extra-file=/dev/fd/99',
'--skip-column-names',
'--batch',
'--execute',
'show schemas',
),
environment=environment,
).and_return('foo\nbar\nmysql\n').once()
names = module.database_names_to_dump({'name': 'all'}, {}, environment, dry_run=False)
names = module.database_names_to_dump(
{'name': 'all'}, {}, 'root', 'trustsome1', environment, dry_run=False
)
assert names == ('foo', 'bar')
def test_database_names_to_dump_runs_mysql_with_tls():
environment = flexmock()
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return((), None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
(
'mysql',
'--defaults-extra-file=/dev/fd/99',
'--ssl',
'--skip-column-names',
'--batch',
'--execute',
'show schemas',
),
environment=environment,
).and_return('foo\nbar\nmysql\n').once()
names = module.database_names_to_dump(
{'name': 'all', 'tls': True}, {}, 'root', 'trustsome1', environment, dry_run=False
)
assert names == ('foo', 'bar')
def test_database_names_to_dump_runs_mysql_without_tls():
environment = flexmock()
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return((), None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
(
'mysql',
'--defaults-extra-file=/dev/fd/99',
'--skip-ssl',
'--skip-column-names',
'--batch',
'--execute',
'show schemas',
),
environment=environment,
).and_return('foo\nbar\nmysql\n').once()
names = module.database_names_to_dump(
{'name': 'all', 'tls': False}, {}, 'root', 'trustsome1', environment, dry_run=False
)
assert names == ('foo', 'bar')
@@ -56,6 +137,9 @@ def test_dump_data_sources_dumps_each_database():
databases = [{'name': 'foo'}, {'name': 'bar'}]
processes = [flexmock(), flexmock()]
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).and_return(None)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
('bar',)
@@ -65,6 +149,8 @@ def test_dump_data_sources_dumps_each_database():
flexmock(module).should_receive('execute_dump_command').with_args(
database={'name': name},
config={},
username=None,
password=None,
dump_path=object,
database_names=(name,),
environment={'USER': 'root'},
@@ -89,10 +175,10 @@ def test_dump_data_sources_dumps_with_password():
database = {'name': 'foo', 'username': 'root', 'password': 'trustsome1'}
process = flexmock()
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
('bar',)
)
@@ -100,9 +186,11 @@ def test_dump_data_sources_dumps_with_password():
flexmock(module).should_receive('execute_dump_command').with_args(
database=database,
config={},
username='root',
password='trustsome1',
dump_path=object,
database_names=('foo',),
environment={'USER': 'root', 'MYSQL_PWD': 'trustsome1'},
environment={'USER': 'root'},
dry_run=object,
dry_run_label=object,
).and_return(process).once()
@@ -121,11 +209,16 @@ def test_dump_data_sources_dumps_all_databases_at_once():
databases = [{'name': 'all'}]
process = flexmock()
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).and_return(None)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
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={},
username=None,
password=None,
dump_path=object,
database_names=('foo', 'bar'),
environment={'USER': 'root'},
@@ -147,6 +240,9 @@ def test_dump_data_sources_dumps_all_databases_separately_when_format_configured
databases = [{'name': 'all', 'format': 'sql'}]
processes = [flexmock(), flexmock()]
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).and_return(None)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('database_names_to_dump').and_return(('foo', 'bar'))
@@ -154,6 +250,8 @@ def test_dump_data_sources_dumps_all_databases_separately_when_format_configured
flexmock(module).should_receive('execute_dump_command').with_args(
database={'name': name, 'format': 'sql'},
config={},
username=None,
password=None,
dump_path=object,
database_names=(name,),
environment={'USER': 'root'},
@@ -175,11 +273,18 @@ def test_dump_data_sources_dumps_all_databases_separately_when_format_configured
def test_database_names_to_dump_runs_mysql_with_list_options():
database = {'name': 'all', 'list_options': '--defaults-extra-file=my.cnf'}
database = {'name': 'all', 'list_options': '--defaults-extra-file=my.cnf --skip-ssl'}
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return(('--skip-ssl',), 'my.cnf')
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args('root', 'trustsome1', 'my.cnf').and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
(
'mysql',
'--defaults-extra-file=my.cnf',
'--defaults-extra-file=/dev/fd/99',
'--skip-ssl',
'--skip-column-names',
'--batch',
'--execute',
@@ -188,20 +293,30 @@ def test_database_names_to_dump_runs_mysql_with_list_options():
environment=None,
).and_return(('foo\nbar')).once()
assert module.database_names_to_dump(database, {}, None, '') == ('foo', 'bar')
assert module.database_names_to_dump(database, {}, 'root', 'trustsome1', None, '') == (
'foo',
'bar',
)
def test_database_names_to_dump_runs_non_default_mysql_with_list_options():
database = {
'name': 'all',
'list_options': '--defaults-extra-file=my.cnf',
'list_options': '--defaults-extra-file=my.cnf --skip-ssl',
'mysql_command': 'custom_mysql',
}
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return(('--skip-ssl',), 'my.cnf')
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args('root', 'trustsome1', 'my.cnf').and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
environment=None,
full_command=(
'custom_mysql', # Custom MySQL command
'--defaults-extra-file=my.cnf',
'--defaults-extra-file=/dev/fd/99',
'--skip-ssl',
'--skip-column-names',
'--batch',
'--execute',
@@ -209,7 +324,10 @@ 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, {}, 'root', 'trustsome1', None, '') == (
'foo',
'bar',
)
def test_execute_dump_command_runs_mysqldump():
@@ -219,11 +337,18 @@ def test_execute_dump_command_runs_mysqldump():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return((), None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
(
'mysqldump',
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--databases',
'foo',
@@ -238,6 +363,8 @@ def test_execute_dump_command_runs_mysqldump():
module.execute_dump_command(
database={'name': 'foo'},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment=None,
@@ -255,11 +382,18 @@ def test_execute_dump_command_runs_mysqldump_without_add_drop_database():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return((), None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
(
'mysqldump',
'--defaults-extra-file=/dev/fd/99',
'--databases',
'foo',
'--result-file',
@@ -273,6 +407,8 @@ def test_execute_dump_command_runs_mysqldump_without_add_drop_database():
module.execute_dump_command(
database={'name': 'foo', 'add_drop_database': False},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment=None,
@@ -290,11 +426,18 @@ def test_execute_dump_command_runs_mysqldump_with_hostname_and_port():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return((), None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
(
'mysqldump',
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--host',
'database.example.org',
@@ -315,6 +458,100 @@ def test_execute_dump_command_runs_mysqldump_with_hostname_and_port():
module.execute_dump_command(
database={'name': 'foo', 'hostname': 'database.example.org', 'port': 5433},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment=None,
dry_run=False,
dry_run_label='',
)
== process
)
def test_execute_dump_command_runs_mysqldump_with_tls():
process = flexmock()
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return((), None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
(
'mysqldump',
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--ssl',
'--databases',
'foo',
'--result-file',
'dump',
),
environment=None,
run_to_completion=False,
).and_return(process).once()
assert (
module.execute_dump_command(
database={'name': 'foo', 'tls': True},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment=None,
dry_run=False,
dry_run_label='',
)
== process
)
def test_execute_dump_command_runs_mysqldump_without_tls():
process = flexmock()
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return((), None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
(
'mysqldump',
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--skip-ssl',
'--databases',
'foo',
'--result-file',
'dump',
),
environment=None,
run_to_completion=False,
).and_return(process).once()
assert (
module.execute_dump_command(
database={'name': 'foo', 'tls': False},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment=None,
@@ -332,20 +569,25 @@ def test_execute_dump_command_runs_mysqldump_with_username_and_password():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return((), None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
(
'mysqldump',
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--user',
'root',
'--databases',
'foo',
'--result-file',
'dump',
),
environment={'MYSQL_PWD': 'trustsome1'},
environment={},
run_to_completion=False,
).and_return(process).once()
@@ -353,9 +595,11 @@ def test_execute_dump_command_runs_mysqldump_with_username_and_password():
module.execute_dump_command(
database={'name': 'foo', 'username': 'root', 'password': 'trustsome1'},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment={'MYSQL_PWD': 'trustsome1'},
environment={},
dry_run=False,
dry_run_label='',
)
@@ -370,11 +614,18 @@ def test_execute_dump_command_runs_mysqldump_with_options():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return(('--stuff=such',), None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
(
'mysqldump',
'--defaults-extra-file=/dev/fd/99',
'--stuff=such',
'--add-drop-database',
'--databases',
@@ -390,6 +641,8 @@ def test_execute_dump_command_runs_mysqldump_with_options():
module.execute_dump_command(
database={'name': 'foo', 'options': '--stuff=such'},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment=None,
@@ -407,11 +660,18 @@ def test_execute_dump_command_runs_non_default_mysqldump():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return((), None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
(
'custom_mysqldump', # Custom MySQL dump command
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--databases',
'foo',
@@ -429,6 +689,8 @@ def test_execute_dump_command_runs_non_default_mysqldump():
'mysql_dump_command': 'custom_mysqldump',
}, # Custom MySQL dump command specified
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment=None,
@@ -442,6 +704,12 @@ def test_execute_dump_command_runs_non_default_mysqldump():
def test_execute_dump_command_with_duplicate_dump_skips_mysqldump():
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return((), None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
flexmock(module).should_receive('execute_command').never()
@@ -449,6 +717,8 @@ def test_execute_dump_command_with_duplicate_dump_skips_mysqldump():
module.execute_dump_command(
database={'name': 'foo'},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment=None,
@@ -465,6 +735,12 @@ def test_execute_dump_command_with_dry_run_skips_mysqldump():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return((), None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').never()
@@ -473,6 +749,8 @@ def test_execute_dump_command_with_dry_run_skips_mysqldump():
module.execute_dump_command(
database={'name': 'foo'},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment=None,
@@ -538,6 +816,12 @@ def test_restore_data_source_dump_runs_mysql_to_restore():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return((), None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args(None, None, None).and_return(())
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
('mysql', '--batch'),
@@ -570,9 +854,15 @@ def test_restore_data_source_dump_runs_mysql_with_options():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return(('--harder',), None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args(None, None, None).and_return(())
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
('mysql', '--batch', '--harder'),
('mysql', '--harder', '--batch'),
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
@@ -602,9 +892,15 @@ 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, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return(('--harder',), None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args(None, None, None).and_return(())
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
('custom_mysql', '--batch', '--harder'),
('custom_mysql', '--harder', '--batch'),
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
@@ -634,6 +930,12 @@ 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, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return((), None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args(None, None, None).and_return(())
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
@@ -668,6 +970,90 @@ def test_restore_data_source_dump_runs_mysql_with_hostname_and_port():
)
def test_restore_data_source_dump_runs_mysql_with_tls():
hook_config = [{'name': 'foo', 'tls': True}]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return((), None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args(None, None, None).and_return(())
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mysql',
'--batch',
'--ssl',
),
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
environment={'USER': 'root'},
).once()
module.restore_data_source_dump(
hook_config,
{},
data_source=hook_config[0],
dry_run=False,
extract_process=extract_process,
connection_params={
'hostname': None,
'port': None,
'username': None,
'password': None,
},
borgmatic_runtime_directory='/run/borgmatic',
)
def test_restore_data_source_dump_runs_mysql_without_tls():
hook_config = [{'name': 'foo', 'tls': False}]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return((), None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args(None, None, None).and_return(())
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mysql',
'--batch',
'--skip-ssl',
),
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
environment={'USER': 'root'},
).once()
module.restore_data_source_dump(
hook_config,
{},
data_source=hook_config[0],
dry_run=False,
extract_process=extract_process,
connection_params={
'hostname': None,
'port': None,
'username': None,
'password': None,
},
borgmatic_runtime_directory='/run/borgmatic',
)
def test_restore_data_source_dump_runs_mysql_with_username_and_password():
hook_config = [{'name': 'foo', 'username': 'root', 'password': 'trustsome1'}]
extract_process = flexmock(stdout=flexmock())
@@ -675,13 +1061,19 @@ 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, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return((), None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
('mysql', '--batch', '--user', 'root'),
('mysql', '--defaults-extra-file=/dev/fd/99', '--batch'),
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
environment={'USER': 'root', 'MYSQL_PWD': 'trustsome1'},
environment={'USER': 'root'},
).once()
module.restore_data_source_dump(
@@ -717,10 +1109,19 @@ 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, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return((), None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args('cliusername', 'clipassword', None).and_return(
('--defaults-extra-file=/dev/fd/99',)
)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mysql',
'--defaults-extra-file=/dev/fd/99',
'--batch',
'--host',
'clihost',
@@ -728,13 +1129,11 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
'cliport',
'--protocol',
'tcp',
'--user',
'cliusername',
),
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
environment={'USER': 'root', 'MYSQL_PWD': 'clipassword'},
environment={'USER': 'root'},
).once()
module.restore_data_source_dump(
@@ -761,10 +1160,12 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
'password': 'trustsome1',
'hostname': 'dbhost',
'port': 'dbport',
'tls': True,
'restore_username': 'restoreuser',
'restore_password': 'restorepass',
'restore_hostname': 'restorehost',
'restore_port': 'restoreport',
'restore_tls': False,
}
]
extract_process = flexmock(stdout=flexmock())
@@ -772,10 +1173,19 @@ 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, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return((), None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args('restoreuser', 'restorepass', None).and_return(
('--defaults-extra-file=/dev/fd/99',)
)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mysql',
'--defaults-extra-file=/dev/fd/99',
'--batch',
'--host',
'restorehost',
@@ -783,13 +1193,12 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
'restoreport',
'--protocol',
'tcp',
'--user',
'restoreuser',
'--skip-ssl',
),
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
environment={'USER': 'root', 'MYSQL_PWD': 'restorepass'},
environment={'USER': 'root'},
).once()
module.restore_data_source_dump(
@@ -814,6 +1223,12 @@ 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, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return((), None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args(None, None, None).and_return(())
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').never()
@@ -555,6 +555,100 @@ def test_dump_data_sources_runs_pg_dump_with_directory_format():
)
def test_dump_data_sources_runs_pg_dump_with_string_compression():
databases = [{'name': 'foo', 'compression': 'winrar'}]
processes = [flexmock()]
flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',))
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
'databases/localhost/foo'
)
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
(
'pg_dump',
'--no-password',
'--clean',
'--if-exists',
'--format',
'custom',
'--compress',
'winrar',
'foo',
'>',
'databases/localhost/foo',
),
shell=True,
environment={'PGSSLMODE': 'disable'},
run_to_completion=False,
).and_return(processes[0]).once()
assert (
module.dump_data_sources(
databases,
{},
config_paths=('test.yaml',),
borgmatic_runtime_directory='/run/borgmatic',
patterns=[],
dry_run=False,
)
== processes
)
def test_dump_data_sources_runs_pg_dump_with_integer_compression():
databases = [{'name': 'foo', 'compression': 0}]
processes = [flexmock()]
flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',))
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
'databases/localhost/foo'
)
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
(
'pg_dump',
'--no-password',
'--clean',
'--if-exists',
'--format',
'custom',
'--compress',
'0',
'foo',
'>',
'databases/localhost/foo',
),
shell=True,
environment={'PGSSLMODE': 'disable'},
run_to_completion=False,
).and_return(processes[0]).once()
assert (
module.dump_data_sources(
databases,
{},
config_paths=('test.yaml',),
borgmatic_runtime_directory='/run/borgmatic',
patterns=[],
dry_run=False,
)
== processes
)
def test_dump_data_sources_runs_pg_dump_with_options():
databases = [{'name': 'foo', 'options': '--stuff=such'}]
process = flexmock()
+18 -2
View File
@@ -220,11 +220,27 @@ def test_get_datasets_to_backup_with_invalid_list_output_raises():
module.get_datasets_to_backup('zfs', patterns=(Pattern('/foo'), Pattern('/bar')))
def test_get_all_dataset_mount_points_does_not_filter_datasets():
def test_get_all_dataset_mount_points_omits_none():
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).and_return(
'/dataset\n/other',
'/dataset\nnone\n/other',
)
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_patterns'
).and_return((Pattern('/dataset'),))
assert module.get_all_dataset_mount_points('zfs') == (
('/dataset'),
('/other'),
)
def test_get_all_dataset_mount_points_omits_duplicates():
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).and_return(
'/dataset\n/other\n/dataset\n/other',
)
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_patterns'
@@ -3,6 +3,46 @@ from flexmock import flexmock
from borgmatic.hooks.monitoring import pagerduty as module
def mock_logger():
logger = flexmock()
logger.should_receive('addHandler')
logger.should_receive('removeHandler')
flexmock(module.logging).should_receive('getLogger').and_return(logger)
def test_initialize_monitor_creates_log_handler():
monitoring_log_level = 1
mock_logger()
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'Forgetful_buffering_handler'
).once()
module.initialize_monitor({}, {}, 'test.yaml', monitoring_log_level, dry_run=False)
def test_initialize_monitor_creates_log_handler_when_send_logs_true():
mock_logger()
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'Forgetful_buffering_handler'
).once()
module.initialize_monitor(
{'send_logs': True}, {}, 'test.yaml', monitoring_log_level=1, dry_run=False
)
def test_initialize_monitor_bails_when_send_logs_false():
mock_logger()
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'Forgetful_buffering_handler'
).never()
module.initialize_monitor(
{'send_logs': False}, {}, 'test.yaml', monitoring_log_level=1, dry_run=False
)
def test_ping_monitor_ignores_start_state():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
@@ -39,6 +79,9 @@ def test_ping_monitor_calls_api_for_fail_state():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload'
).and_return('loggy\nlogs')
flexmock(module.requests).should_receive('post').and_return(flexmock(ok=True))
module.ping_monitor(
@@ -55,6 +98,9 @@ def test_ping_monitor_dry_run_does_not_call_api():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload'
).and_return('loggy\nlogs')
flexmock(module.requests).should_receive('post').never()
module.ping_monitor(
@@ -71,6 +117,9 @@ def test_ping_monitor_with_connection_error_logs_warning():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload'
).and_return('loggy\nlogs')
flexmock(module.requests).should_receive('post').and_raise(
module.requests.exceptions.ConnectionError
)
@@ -108,6 +157,9 @@ def test_ping_monitor_with_other_error_logs_warning():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload'
).and_return('loggy\nlogs')
response.should_receive('raise_for_status').and_raise(
module.requests.exceptions.RequestException
)
+38 -6
View File
@@ -10,7 +10,7 @@ CUSTOM_PUSH_URL = 'https://uptime.example.com/api/push/efgh5678'
def test_ping_monitor_hits_default_uptimekuma_on_fail():
hook_config = {}
flexmock(module.requests).should_receive('get').with_args(
f'{DEFAULT_PUSH_URL}?status=down&msg=fail'
f'{DEFAULT_PUSH_URL}?status=down&msg=fail', verify=True
).and_return(flexmock(ok=True)).once()
module.ping_monitor(
@@ -26,7 +26,7 @@ def test_ping_monitor_hits_default_uptimekuma_on_fail():
def test_ping_monitor_hits_custom_uptimekuma_on_fail():
hook_config = {'push_url': CUSTOM_PUSH_URL}
flexmock(module.requests).should_receive('get').with_args(
f'{CUSTOM_PUSH_URL}?status=down&msg=fail'
f'{CUSTOM_PUSH_URL}?status=down&msg=fail', verify=True
).and_return(flexmock(ok=True)).once()
module.ping_monitor(
@@ -42,7 +42,7 @@ def test_ping_monitor_hits_custom_uptimekuma_on_fail():
def test_ping_monitor_custom_uptimekuma_on_start():
hook_config = {'push_url': CUSTOM_PUSH_URL}
flexmock(module.requests).should_receive('get').with_args(
f'{CUSTOM_PUSH_URL}?status=up&msg=start'
f'{CUSTOM_PUSH_URL}?status=up&msg=start', verify=True
).and_return(flexmock(ok=True)).once()
module.ping_monitor(
@@ -58,7 +58,7 @@ def test_ping_monitor_custom_uptimekuma_on_start():
def test_ping_monitor_custom_uptimekuma_on_finish():
hook_config = {'push_url': CUSTOM_PUSH_URL}
flexmock(module.requests).should_receive('get').with_args(
f'{CUSTOM_PUSH_URL}?status=up&msg=finish'
f'{CUSTOM_PUSH_URL}?status=up&msg=finish', verify=True
).and_return(flexmock(ok=True)).once()
module.ping_monitor(
@@ -116,7 +116,7 @@ def test_ping_monitor_does_not_hit_custom_uptimekuma_on_finish_dry_run():
def test_ping_monitor_with_connection_error_logs_warning():
hook_config = {'push_url': CUSTOM_PUSH_URL}
flexmock(module.requests).should_receive('get').with_args(
f'{CUSTOM_PUSH_URL}?status=down&msg=fail'
f'{CUSTOM_PUSH_URL}?status=down&msg=fail', verify=True
).and_raise(module.requests.exceptions.ConnectionError)
flexmock(module.logger).should_receive('warning').once()
@@ -137,7 +137,7 @@ def test_ping_monitor_with_other_error_logs_warning():
module.requests.exceptions.RequestException
)
flexmock(module.requests).should_receive('get').with_args(
f'{CUSTOM_PUSH_URL}?status=down&msg=fail'
f'{CUSTOM_PUSH_URL}?status=down&msg=fail', verify=True
).and_return(response)
flexmock(module.logger).should_receive('warning').once()
@@ -163,3 +163,35 @@ def test_ping_monitor_with_invalid_run_state():
monitoring_log_level=1,
dry_run=True,
)
def test_ping_monitor_skips_ssl_verification_when_verify_tls_false():
hook_config = {'push_url': CUSTOM_PUSH_URL, 'verify_tls': False}
flexmock(module.requests).should_receive('get').with_args(
f'{CUSTOM_PUSH_URL}?status=down&msg=fail', verify=False
).and_return(flexmock(ok=True)).once()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_executes_ssl_verification_when_verify_tls_true():
hook_config = {'push_url': CUSTOM_PUSH_URL, 'verify_tls': True}
flexmock(module.requests).should_receive('get').with_args(
f'{CUSTOM_PUSH_URL}?status=down&msg=fail', verify=True
).and_return(flexmock(ok=True)).once()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
+169 -102
View File
@@ -24,7 +24,6 @@ DATA_HOST_KEY_WITH_KEY_VALUE = {
'method': 'history.push',
'params': {'host': HOST, 'key': KEY, 'value': VALUE},
'id': 1,
'auth': '3fe6ed01a69ebd79907a120bcd04e494',
}
DATA_ITEMID = {
@@ -39,7 +38,6 @@ DATA_HOST_KEY_WITH_ITEMID = {
'method': 'history.push',
'params': {'itemid': ITEMID, 'value': VALUE},
'id': 1,
'auth': '3fe6ed01a69ebd79907a120bcd04e494',
}
DATA_USER_LOGIN = {
@@ -49,17 +47,99 @@ DATA_USER_LOGIN = {
'id': 1,
}
AUTH_HEADERS_API_KEY = {
DATA_USER_LOGOUT = {
'jsonrpc': '2.0',
'method': 'user.logout',
'params': [],
'id': 1,
}
AUTH_HEADERS_LOGIN = {
'Content-Type': 'application/json-rpc',
}
AUTH_HEADERS = {
'Content-Type': 'application/json-rpc',
'Authorization': f'Bearer {API_KEY}',
}
AUTH_HEADERS_USERNAME_PASSWORD = {'Content-Type': 'application/json-rpc'}
def test_send_zabbix_request_with_post_error_bails():
server = flexmock()
headers = flexmock()
data = {'method': 'do.stuff'}
response = flexmock(ok=False)
response.should_receive('raise_for_status').and_raise(
module.requests.exceptions.RequestException
)
flexmock(module.requests).should_receive('post').with_args(
server, headers=headers, json=data
).and_return(response)
assert module.send_zabbix_request(server, headers, data) is None
def test_send_zabbix_request_with_invalid_json_response_bails():
server = flexmock()
headers = flexmock()
data = {'method': 'do.stuff'}
flexmock(module.requests.exceptions.JSONDecodeError).should_receive('__init__')
response = flexmock(ok=True)
response.should_receive('json').and_raise(module.requests.exceptions.JSONDecodeError)
flexmock(module.requests).should_receive('post').with_args(
server, headers=headers, json=data
).and_return(response)
assert module.send_zabbix_request(server, headers, data) is None
def test_send_zabbix_request_with_success_returns_response_result():
server = flexmock()
headers = flexmock()
data = {'method': 'do.stuff'}
response = flexmock(ok=True)
response.should_receive('json').and_return({'result': {'foo': 'bar'}})
flexmock(module.requests).should_receive('post').with_args(
server, headers=headers, json=data
).and_return(response)
assert module.send_zabbix_request(server, headers, data) == {'foo': 'bar'}
def test_send_zabbix_request_with_success_passes_through_missing_result():
server = flexmock()
headers = flexmock()
data = {'method': 'do.stuff'}
response = flexmock(ok=True)
response.should_receive('json').and_return({})
flexmock(module.requests).should_receive('post').with_args(
server, headers=headers, json=data
).and_return(response)
assert module.send_zabbix_request(server, headers, data) is None
def test_send_zabbix_request_with_error_bails():
server = flexmock()
headers = flexmock()
data = {'method': 'do.stuff'}
response = flexmock(ok=True)
response.should_receive('json').and_return({'result': {'data': [{'error': 'oops'}]}})
flexmock(module.requests).should_receive('post').with_args(
server, headers=headers, json=data
).and_return(response)
assert module.send_zabbix_request(server, headers, data) is None
def test_ping_monitor_with_non_matching_state_bails():
hook_config = {'api_key': API_KEY}
flexmock(module.requests).should_receive('post').never()
flexmock(module).should_receive('send_zabbix_request').never()
module.ping_monitor(
hook_config,
@@ -79,7 +159,7 @@ def test_ping_monitor_config_with_api_key_only_bails():
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
flexmock(module).should_receive('send_zabbix_request').never()
module.ping_monitor(
hook_config,
@@ -99,7 +179,7 @@ def test_ping_monitor_config_with_host_only_bails():
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
flexmock(module).should_receive('send_zabbix_request').never()
module.ping_monitor(
hook_config,
@@ -119,7 +199,7 @@ def test_ping_monitor_config_with_key_only_bails():
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
flexmock(module).should_receive('send_zabbix_request').never()
module.ping_monitor(
hook_config,
@@ -139,7 +219,7 @@ def test_ping_monitor_config_with_server_only_bails():
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
flexmock(module).should_receive('send_zabbix_request').never()
module.ping_monitor(
hook_config,
@@ -158,7 +238,7 @@ def test_ping_monitor_config_user_password_no_zabbix_data_bails():
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
flexmock(module).should_receive('send_zabbix_request').never()
module.ping_monitor(
hook_config,
@@ -177,7 +257,7 @@ def test_ping_monitor_config_api_key_no_zabbix_data_bails():
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
flexmock(module).should_receive('send_zabbix_request').never()
module.ping_monitor(
hook_config,
@@ -197,7 +277,7 @@ def test_ping_monitor_config_itemid_no_auth_data_bails():
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
flexmock(module).should_receive('send_zabbix_request').never()
module.ping_monitor(
hook_config,
@@ -217,7 +297,7 @@ def test_ping_monitor_config_host_and_key_no_auth_data_bails():
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
flexmock(module).should_receive('send_zabbix_request').never()
module.ping_monitor(
hook_config,
@@ -236,11 +316,35 @@ def test_ping_monitor_config_host_and_key_with_api_key_auth_data_successful():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.requests).should_receive('post').with_args(
flexmock(module).should_receive('send_zabbix_request').with_args(
f'{SERVER}',
headers=AUTH_HEADERS_API_KEY,
json=DATA_HOST_KEY,
).and_return(flexmock(ok=True)).once()
headers=AUTH_HEADERS,
data=DATA_HOST_KEY,
).once()
flexmock(module.logger).should_receive('warning').never()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_config_adds_missing_api_endpoint_to_server_url():
# This test should simulate a successful POST to a Zabbix server. This test uses API_KEY
# to authenticate and HOST/KEY to know which item to populate in Zabbix.
hook_config = {'server': SERVER, 'host': HOST, 'key': KEY, 'api_key': API_KEY}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module).should_receive('send_zabbix_request').with_args(
f'{SERVER}',
headers=AUTH_HEADERS,
data=DATA_HOST_KEY,
).once()
flexmock(module.logger).should_receive('warning').never()
module.ping_monitor(
@@ -259,7 +363,7 @@ def test_ping_monitor_config_host_and_missing_key_bails():
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
flexmock(module).should_receive('send_zabbix_request').never()
module.ping_monitor(
hook_config,
@@ -277,7 +381,7 @@ def test_ping_monitor_config_key_and_missing_host_bails():
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
flexmock(module).should_receive('send_zabbix_request').never()
module.ping_monitor(
hook_config,
@@ -303,24 +407,26 @@ 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, config: value)
auth_response = flexmock(ok=True)
auth_response.should_receive('json').and_return(
{'jsonrpc': '2.0', 'result': '3fe6ed01a69ebd79907a120bcd04e494', 'id': 1}
)
flexmock(module.requests).should_receive('post').with_args(
flexmock(module).should_receive('send_zabbix_request').with_args(
f'{SERVER}',
headers=AUTH_HEADERS_USERNAME_PASSWORD,
json=DATA_USER_LOGIN,
).and_return(auth_response).once()
headers=AUTH_HEADERS_LOGIN,
data=DATA_USER_LOGIN,
).and_return('fakekey').once()
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').with_args(
flexmock(module).should_receive('send_zabbix_request').with_args(
f'{SERVER}',
headers=AUTH_HEADERS_USERNAME_PASSWORD,
json=DATA_HOST_KEY_WITH_KEY_VALUE,
).and_return(flexmock(ok=True)).once()
headers=AUTH_HEADERS,
data=DATA_HOST_KEY_WITH_KEY_VALUE,
).once()
flexmock(module).should_receive('send_zabbix_request').with_args(
f'{SERVER}',
headers=AUTH_HEADERS,
data=DATA_USER_LOGOUT,
).once()
module.ping_monitor(
hook_config,
@@ -344,24 +450,22 @@ 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, config: value)
auth_response = flexmock(ok=False)
auth_response.should_receive('json').and_return(
{'jsonrpc': '2.0', 'result': '3fe6ed01a69ebd79907a120bcd04e494', 'id': 1}
)
auth_response.should_receive('raise_for_status').and_raise(
module.requests.ConnectionError
).once()
flexmock(module.requests).should_receive('post').with_args(
flexmock(module).should_receive('send_zabbix_request').with_args(
f'{SERVER}',
headers=AUTH_HEADERS_USERNAME_PASSWORD,
json=DATA_USER_LOGIN,
).and_return(auth_response).once()
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').with_args(
headers=AUTH_HEADERS_LOGIN,
data=DATA_USER_LOGIN,
).and_return(None).once()
flexmock(module).should_receive('send_zabbix_request').with_args(
f'{SERVER}',
headers=AUTH_HEADERS_USERNAME_PASSWORD,
json=DATA_HOST_KEY_WITH_KEY_VALUE,
headers=AUTH_HEADERS,
data=DATA_HOST_KEY_WITH_KEY_VALUE,
).never()
flexmock(module).should_receive('send_zabbix_request').with_args(
f'{SERVER}',
headers=AUTH_HEADERS,
data=DATA_USER_LOGOUT,
).never()
module.ping_monitor(
@@ -386,7 +490,7 @@ def test_ping_monitor_config_host_and_key_with_username_and_missing_password_bai
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
flexmock(module).should_receive('send_zabbix_request').never()
module.ping_monitor(
hook_config,
@@ -410,7 +514,7 @@ def test_ping_monitor_config_host_and_key_with_password_and_missing_username_bai
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
flexmock(module).should_receive('send_zabbix_request').never()
module.ping_monitor(
hook_config,
@@ -429,11 +533,11 @@ def test_ping_monitor_config_itemid_with_api_key_auth_data_successful():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.requests).should_receive('post').with_args(
flexmock(module).should_receive('send_zabbix_request').with_args(
f'{SERVER}',
headers=AUTH_HEADERS_API_KEY,
json=DATA_ITEMID,
).and_return(flexmock(ok=True)).once()
headers=AUTH_HEADERS,
data=DATA_ITEMID,
).once()
flexmock(module.logger).should_receive('warning').never()
module.ping_monitor(
@@ -454,63 +558,26 @@ 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, config: value)
auth_response = flexmock(ok=True)
auth_response.should_receive('json').and_return(
{'jsonrpc': '2.0', 'result': '3fe6ed01a69ebd79907a120bcd04e494', 'id': 1}
)
flexmock(module.requests).should_receive('post').with_args(
flexmock(module).should_receive('send_zabbix_request').with_args(
f'{SERVER}',
headers=AUTH_HEADERS_USERNAME_PASSWORD,
json=DATA_USER_LOGIN,
).and_return(auth_response).once()
headers=AUTH_HEADERS_LOGIN,
data=DATA_USER_LOGIN,
).and_return('fakekey').once()
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').with_args(
flexmock(module).should_receive('send_zabbix_request').with_args(
f'{SERVER}',
headers=AUTH_HEADERS_USERNAME_PASSWORD,
json=DATA_HOST_KEY_WITH_ITEMID,
).and_return(flexmock(ok=True)).once()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_config_itemid_with_username_password_auth_data_and_push_post_error_bails():
hook_config = {'server': SERVER, 'itemid': ITEMID, 'username': USERNAME, 'password': PASSWORD}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
auth_response = flexmock(ok=True)
auth_response.should_receive('json').and_return(
{'jsonrpc': '2.0', 'result': '3fe6ed01a69ebd79907a120bcd04e494', 'id': 1}
)
flexmock(module.requests).should_receive('post').with_args(
f'{SERVER}',
headers=AUTH_HEADERS_USERNAME_PASSWORD,
json=DATA_USER_LOGIN,
).and_return(auth_response).once()
push_response = flexmock(ok=False)
push_response.should_receive('raise_for_status').and_raise(
module.requests.ConnectionError
headers=AUTH_HEADERS,
data=DATA_HOST_KEY_WITH_ITEMID,
).once()
flexmock(module.requests).should_receive('post').with_args(
f'{SERVER}',
headers=AUTH_HEADERS_USERNAME_PASSWORD,
json=DATA_HOST_KEY_WITH_ITEMID,
).and_return(push_response).once()
flexmock(module.logger).should_receive('warning').once()
flexmock(module).should_receive('send_zabbix_request').with_args(
f'{SERVER}',
headers=AUTH_HEADERS,
data=DATA_USER_LOGOUT,
).once()
module.ping_monitor(
hook_config,
@@ -528,7 +595,7 @@ def test_ping_monitor_with_credential_error_bails():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).and_raise(ValueError)
flexmock(module.requests).should_receive('post').never()
flexmock(module).should_receive('send_zabbix_request').never()
flexmock(module.logger).should_receive('warning').once()
module.ping_monitor(
+31 -93
View File
@@ -191,7 +191,7 @@ def test_execute_command_calls_full_command():
shell=False,
env=None,
cwd=None,
close_fds=True,
close_fds=False,
).and_return(flexmock(stdout=None)).once()
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('log_outputs')
@@ -213,7 +213,7 @@ def test_execute_command_calls_full_command_with_output_file():
shell=False,
env=None,
cwd=None,
close_fds=True,
close_fds=False,
).and_return(flexmock(stderr=None)).once()
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('log_outputs')
@@ -234,7 +234,7 @@ def test_execute_command_calls_full_command_without_capturing_output():
shell=False,
env=None,
cwd=None,
close_fds=True,
close_fds=False,
).and_return(flexmock(wait=lambda: 0)).once()
flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.SUCCESS)
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
@@ -257,7 +257,7 @@ def test_execute_command_calls_full_command_with_input_file():
shell=False,
env=None,
cwd=None,
close_fds=True,
close_fds=False,
).and_return(flexmock(stdout=None)).once()
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('log_outputs')
@@ -278,7 +278,7 @@ def test_execute_command_calls_full_command_with_shell():
shell=True,
env=None,
cwd=None,
close_fds=True,
close_fds=False,
).and_return(flexmock(stdout=None)).once()
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('log_outputs')
@@ -299,7 +299,7 @@ def test_execute_command_calls_full_command_with_environment():
shell=False,
env={'a': 'b'},
cwd=None,
close_fds=True,
close_fds=False,
).and_return(flexmock(stdout=None)).once()
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('log_outputs')
@@ -320,33 +320,12 @@ def test_execute_command_calls_full_command_with_working_directory():
shell=False,
env=None,
cwd='/working',
close_fds=True,
).and_return(flexmock(stdout=None)).once()
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('log_outputs')
output = module.execute_command(full_command, working_directory='/working')
assert output is None
def test_execute_command_with_BORG_PASSPHRASE_FD_leaves_file_descriptors_open():
full_command = ['foo', 'bar']
flexmock(module).should_receive('log_command')
flexmock(module.subprocess).should_receive('Popen').with_args(
full_command,
stdin=None,
stdout=module.subprocess.PIPE,
stderr=module.subprocess.STDOUT,
shell=False,
env={'BORG_PASSPHRASE_FD': '4'},
cwd=None,
close_fds=False,
).and_return(flexmock(stdout=None)).once()
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('log_outputs')
output = module.execute_command(full_command, environment={'BORG_PASSPHRASE_FD': '4'})
output = module.execute_command(full_command, working_directory='/working')
assert output is None
@@ -363,7 +342,7 @@ def test_execute_command_without_run_to_completion_returns_process():
shell=False,
env=None,
cwd=None,
close_fds=True,
close_fds=False,
).and_return(process).once()
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('log_outputs')
@@ -377,11 +356,12 @@ def test_execute_command_and_capture_output_returns_stdout():
flexmock(module).should_receive('log_command')
flexmock(module.subprocess).should_receive('check_output').with_args(
full_command,
stdin=None,
stderr=None,
shell=False,
env=None,
cwd=None,
close_fds=True,
close_fds=False,
).and_return(flexmock(decode=lambda: expected_output)).once()
output = module.execute_command_and_capture_output(full_command)
@@ -395,11 +375,12 @@ def test_execute_command_and_capture_output_with_capture_stderr_returns_stderr()
flexmock(module).should_receive('log_command')
flexmock(module.subprocess).should_receive('check_output').with_args(
full_command,
stdin=None,
stderr=module.subprocess.STDOUT,
shell=False,
env=None,
cwd=None,
close_fds=True,
close_fds=False,
).and_return(flexmock(decode=lambda: expected_output)).once()
output = module.execute_command_and_capture_output(full_command, capture_stderr=True)
@@ -414,11 +395,12 @@ def test_execute_command_and_capture_output_returns_output_when_process_error_is
flexmock(module).should_receive('log_command')
flexmock(module.subprocess).should_receive('check_output').with_args(
full_command,
stdin=None,
stderr=None,
shell=False,
env=None,
cwd=None,
close_fds=True,
close_fds=False,
).and_raise(subprocess.CalledProcessError(1, full_command, err_output)).once()
flexmock(module).should_receive('interpret_exit_code').and_return(
module.Exit_status.SUCCESS
@@ -435,11 +417,12 @@ def test_execute_command_and_capture_output_raises_when_command_errors():
flexmock(module).should_receive('log_command')
flexmock(module.subprocess).should_receive('check_output').with_args(
full_command,
stdin=None,
stderr=None,
shell=False,
env=None,
cwd=None,
close_fds=True,
close_fds=False,
).and_raise(subprocess.CalledProcessError(2, full_command, expected_output)).once()
flexmock(module).should_receive('interpret_exit_code').and_return(
module.Exit_status.ERROR
@@ -455,11 +438,12 @@ def test_execute_command_and_capture_output_returns_output_with_shell():
flexmock(module).should_receive('log_command')
flexmock(module.subprocess).should_receive('check_output').with_args(
'foo bar',
stdin=None,
stderr=None,
shell=True,
env=None,
cwd=None,
close_fds=True,
close_fds=False,
).and_return(flexmock(decode=lambda: expected_output)).once()
output = module.execute_command_and_capture_output(full_command, shell=True)
@@ -473,11 +457,12 @@ def test_execute_command_and_capture_output_returns_output_with_environment():
flexmock(module).should_receive('log_command')
flexmock(module.subprocess).should_receive('check_output').with_args(
full_command,
stdin=None,
stderr=None,
shell=False,
env={'a': 'b'},
cwd=None,
close_fds=True,
close_fds=False,
).and_return(flexmock(decode=lambda: expected_output)).once()
output = module.execute_command_and_capture_output(
@@ -493,37 +478,16 @@ def test_execute_command_and_capture_output_returns_output_with_working_director
flexmock(module).should_receive('log_command')
flexmock(module.subprocess).should_receive('check_output').with_args(
full_command,
stdin=None,
stderr=None,
shell=False,
env=None,
cwd='/working',
close_fds=True,
).and_return(flexmock(decode=lambda: expected_output)).once()
output = module.execute_command_and_capture_output(
full_command, shell=False, working_directory='/working'
)
assert output == expected_output
def test_execute_command_and_capture_output_with_BORG_PASSPHRASE_FD_leaves_file_descriptors_open():
full_command = ['foo', 'bar']
expected_output = '[]'
flexmock(module).should_receive('log_command')
flexmock(module.subprocess).should_receive('check_output').with_args(
full_command,
stderr=None,
shell=False,
env={'BORG_PASSPHRASE_FD': '4'},
cwd=None,
close_fds=False,
).and_return(flexmock(decode=lambda: expected_output)).once()
output = module.execute_command_and_capture_output(
full_command,
shell=False,
environment={'BORG_PASSPHRASE_FD': '4'},
full_command, shell=False, working_directory='/working'
)
assert output == expected_output
@@ -541,7 +505,7 @@ def test_execute_command_with_processes_calls_full_command():
shell=False,
env=None,
cwd=None,
close_fds=True,
close_fds=False,
).and_return(flexmock(stdout=None)).once()
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('log_outputs')
@@ -564,7 +528,7 @@ def test_execute_command_with_processes_returns_output_with_output_log_level_non
shell=False,
env=None,
cwd=None,
close_fds=True,
close_fds=False,
).and_return(process).once()
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('log_outputs').and_return({process: 'out'})
@@ -587,7 +551,7 @@ def test_execute_command_with_processes_calls_full_command_with_output_file():
shell=False,
env=None,
cwd=None,
close_fds=True,
close_fds=False,
).and_return(flexmock(stderr=None)).once()
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('log_outputs')
@@ -609,7 +573,7 @@ def test_execute_command_with_processes_calls_full_command_without_capturing_out
shell=False,
env=None,
cwd=None,
close_fds=True,
close_fds=False,
).and_return(flexmock(wait=lambda: 0)).once()
flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.SUCCESS)
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
@@ -635,7 +599,7 @@ def test_execute_command_with_processes_calls_full_command_with_input_file():
shell=False,
env=None,
cwd=None,
close_fds=True,
close_fds=False,
).and_return(flexmock(stdout=None)).once()
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('log_outputs')
@@ -657,7 +621,7 @@ def test_execute_command_with_processes_calls_full_command_with_shell():
shell=True,
env=None,
cwd=None,
close_fds=True,
close_fds=False,
).and_return(flexmock(stdout=None)).once()
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('log_outputs')
@@ -679,7 +643,7 @@ def test_execute_command_with_processes_calls_full_command_with_environment():
shell=False,
env={'a': 'b'},
cwd=None,
close_fds=True,
close_fds=False,
).and_return(flexmock(stdout=None)).once()
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('log_outputs')
@@ -701,39 +665,13 @@ def test_execute_command_with_processes_calls_full_command_with_working_director
shell=False,
env=None,
cwd='/working',
close_fds=True,
).and_return(flexmock(stdout=None)).once()
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('log_outputs')
output = module.execute_command_with_processes(
full_command, processes, working_directory='/working'
)
assert output is None
def test_execute_command_with_processes_with_BORG_PASSPHRASE_FD_leaves_file_descriptors_open():
full_command = ['foo', 'bar']
processes = (flexmock(),)
flexmock(module).should_receive('log_command')
flexmock(module.subprocess).should_receive('Popen').with_args(
full_command,
stdin=None,
stdout=module.subprocess.PIPE,
stderr=module.subprocess.STDOUT,
shell=False,
env={'BORG_PASSPHRASE_FD': '4'},
cwd=None,
close_fds=False,
).and_return(flexmock(stdout=None)).once()
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('log_outputs')
output = module.execute_command_with_processes(
full_command,
processes,
environment={'BORG_PASSPHRASE_FD': '4'},
full_command, processes, working_directory='/working'
)
assert output is None
@@ -754,7 +692,7 @@ def test_execute_command_with_processes_kills_processes_on_error():
shell=False,
env=None,
cwd=None,
close_fds=True,
close_fds=False,
).and_raise(subprocess.CalledProcessError(1, full_command, 'error')).once()
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
flexmock(module).should_receive('log_outputs').never()
+5 -3
View File
@@ -26,7 +26,7 @@ def test_handle_signal_bails_on_recursion():
def test_handle_signal_exits_on_sigterm():
signal_number = module.signal.SIGTERM
frame = flexmock(f_back=flexmock(f_code=flexmock(co_name='something')))
flexmock(module.os).should_receive('getpgrp').and_return(flexmock)
flexmock(module.os).should_receive('getpgrp').and_return(flexmock())
flexmock(module.os).should_receive('killpg')
flexmock(module.sys).should_receive('exit').with_args(
module.EXIT_CODE_FROM_SIGNAL + signal_number
@@ -38,8 +38,10 @@ def test_handle_signal_exits_on_sigterm():
def test_handle_signal_raises_on_sigint():
signal_number = module.signal.SIGINT
frame = flexmock(f_back=flexmock(f_code=flexmock(co_name='something')))
flexmock(module.os).should_receive('getpgrp').and_return(flexmock)
flexmock(module.os).should_receive('killpg')
process_group = flexmock()
flexmock(module.os).should_receive('getpgrp').and_return(process_group)
flexmock(module.os).should_receive('killpg').with_args(process_group, module.signal.SIGINT)
flexmock(module.os).should_receive('killpg').with_args(process_group, module.signal.SIGTERM)
flexmock(module.sys).should_receive('exit').never()
with pytest.raises(KeyboardInterrupt):