Compare commits

..
38 Commits
Author SHA1 Message Date
Dan Helfman 4f88018558 Bump version for release. 2025-02-22 14:39:45 -08:00
Dan Helfman 3642687ab5 Fix broken tests (#999). 2025-02-22 14:32:32 -08:00
Dan Helfman 5d9c111910 Fix a runtime directory error from a conflict between "extra_borg_options" and special file detection (#999). 2025-02-22 14:26:21 -08:00
Dan Helfman 3cf19dd1b0 Send the "encryption_passphrase" option to Borg via an anonymous pipe (#998).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/998
2025-02-22 17:57:37 +00:00
Dan Helfman ad3392ca15 Ignore the BORG_PASSCOMMAND environment variable when the "encryption_passphase" option is set. 2025-02-22 09:55:07 -08:00
Dan Helfman 087b7f5c7b Merge branch 'main' into passphrase-via-file-descriptor 2025-02-22 09:27:39 -08:00
Dan Helfman 34bb09e9be Document Zabbix server version compatibility (#1003). 2025-02-22 09:26:08 -08:00
Dan Helfman a61eba8c79 Add PR number to NEWS item. 2025-02-21 22:30:31 -08:00
Dan Helfman 2280bb26b6 Fix a few tests to mock more accurately. 2025-02-21 22:08:08 -08:00
Dan Helfman 4ee2603fef Merge branch 'main' into passphrase-via-file-descriptor 2025-02-21 20:26:48 -08:00
Dan Helfman cc2ede70ac Fix ZFS mount errors (#1001).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/1002
2025-02-22 04:13:35 +00:00
Dan Helfman 02d8ecd66e Document the root pattern requirement for snapshotting (#1001). 2025-02-21 18:08:34 -08:00
Dan Helfman 9ba78fa33b Don't try to unmount empty directories (#1001). 2025-02-21 17:59:45 -08:00
Dan Helfman a3e34d63e9 Remove debugging prints (#1001). 2025-02-21 16:36:12 -08:00
Dan Helfman bc25ac4eea Fix Btrfs end-to-end-test (#1001). 2025-02-21 16:32:07 -08:00
Dan Helfman e69c686abf Get all unit/integration tests passing (#1001). 2025-02-21 11:32:57 -08:00
Dan Helfman 0210bf76bc Fix ZFS and Btrfs tests (#1001). 2025-02-20 22:58:05 -08:00
Dan Helfman e69cce7e51 Document ZFS snapshotting exclusion of "canmount=off" datasets (#1001). 2025-02-20 14:04:23 -08:00
Dan Helfman 3655e8784a Add NEWS items for filesystem hook fixes/changes (#1001). 2025-02-20 13:25:09 -08:00
Dan Helfman 58aed0892c Initial work on fixing ZFS mount errors (#1001). 2025-02-19 22:49:14 -08:00
Dan Helfman 0e65169503 Improve clarity of comments and variable names of runtime directory exclude detection logic (#999). 2025-02-17 14:12:55 -08:00
Dan Helfman 07ecc0ffd6 Send the "encryption_passphrase" option to Borg via an anonymous pipe. 2025-02-17 11:03:36 -08:00
Dan Helfman 37ad398aff Add a ticket number to NEWS for (some of) the credential hook work. 2025-02-16 09:12:52 -08:00
Dan Helfman 056dfc6d33 Add Btrfs "/" subvolume fix to NEWS. 2025-02-15 09:56:46 -08:00
Dan HelfmanandGitHub bf850b9d38 Fix path handling error when handling btrfs '/' subvolume.
Merge pull request #89 from dmitry-t7ko/btrfs-root-submodule-fix
2025-02-15 09:49:13 -08:00
Dan Helfman 7f22612bf1 Add credential loading from file, KeePassXC, and Docker/Podman secrets.
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/994
2025-02-15 04:20:11 +00:00
Dan Helfman e02a0e6322 Support working directory for container and file credential hooks. 2025-02-14 19:35:12 -08:00
Dan Helfman 2ca23b629c Add end-to-end tests for new credential hooks, along with some related configuration options. 2025-02-14 15:33:30 -08:00
Dan Helfman b283e379d0 Actually pass the current configuration to credential hooks. 2025-02-14 10:15:52 -08:00
Dan Helfman 5dda9c8ee5 Add unit tests for new credential hooks. 2025-02-13 16:38:50 -08:00
Dmitrii Tishchenko 653d8c0946 Remove unneeded 'continue' 2025-02-13 21:44:45 +00:00
Dmitrii Tishchenko 92e87d839d Fix path handling error when handling btrfs '/' submodule 2025-02-13 17:12:23 +00:00
Dan Helfman d6cf48544a Get existing tests passing. 2025-02-12 22:49:16 -08:00
Dan Helfman 8745b9939d Add documentation for new credential hooks. 2025-02-12 21:44:17 -08:00
Dan Helfman 5661b67cde Merge branch 'main' into keepassxc-docker-podman-file-credentials 2025-02-12 09:14:49 -08:00
Dan Helfman aa4a9de3b2 Fix the "create" action to omit the repository label prefix from Borg's output when databases are enabled (#996). 2025-02-12 09:12:59 -08:00
Dan Helfman f9ea45493d Add missing dev0 tag to version. 2025-02-11 23:00:26 -08:00
Dan Helfman a0ba5b673b Add credential loading from file, KeePassXC, and Docker/Podman secrets. 2025-02-11 22:54:07 -08:00
107 changed files with 2957 additions and 1156 deletions
+19
View File
@@ -1,3 +1,22 @@
1.9.11
* #795: Add credential loading from file, KeePassXC, and Docker/Podman secrets. See the
documentation for more information:
https://torsion.org/borgmatic/docs/how-to/provide-your-passwords/
* #996: Fix the "create" action to omit the repository label prefix from Borg's output when
databases are enabled.
* #998: Send the "encryption_passphrase" option to Borg via an anonymous pipe, which is more secure
than using an environment variable.
* #999: Fix a runtime directory error from a conflict between "extra_borg_options" and special file
detection.
* #1001: For the ZFS, Btrfs, and LVM hooks, only make snapshots for root patterns that come from
a borgmatic configuration option (e.g. "source_directories")—not from other hooks within
borgmatic.
* #1001: Fix a ZFS/LVM error due to colliding snapshot mount points for nested datasets or logical
volumes.
* #1001: Don't try to snapshot ZFS datasets that have the "canmount=off" property.
* Fix another error in the Btrfs hook when a subvolume mounted at "/" is configured in borgmatic's
source directories.
1.9.10
* #966: Add a "{credential ...}" syntax for loading systemd credentials into borgmatic
configuration files. See the documentation for more information:
+3
View File
@@ -88,6 +88,9 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
### Credentials
<a href="https://systemd.io/"><img src="docs/static/systemd.png" alt="Sentry" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://www.docker.com/"><img src="docs/static/docker.png" alt="Docker" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://podman.io/"><img src="docs/static/podman.png" alt="Podman" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://keepassxc.org/"><img src="docs/static/keepassxc.png" alt="Podman" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
## Getting started
+1 -1
View File
@@ -16,7 +16,7 @@ def run_change_passphrase(
remote_path,
):
'''
Run the "key change-passprhase" action for the given repository.
Run the "key change-passphrase" action for the given repository.
'''
if (
change_passphrase_arguments.repository is None
+1 -1
View File
@@ -391,7 +391,7 @@ def collect_spot_check_source_paths(
paths_output = borgmatic.execute.execute_command_and_capture_output(
create_flags + create_positional_arguments,
capture_stderr=True,
extra_environment=borgmatic.borg.environment.make_environment(config),
environment=borgmatic.borg.environment.make_environment(config),
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
+6 -1
View File
@@ -36,6 +36,7 @@ def parse_pattern(pattern_line, default_style=borgmatic.borg.pattern.Pattern_sty
path,
borgmatic.borg.pattern.Pattern_type(pattern_type),
borgmatic.borg.pattern.Pattern_style(pattern_style),
source=borgmatic.borg.pattern.Pattern_source.CONFIG,
)
@@ -51,7 +52,9 @@ def collect_patterns(config):
try:
return (
tuple(
borgmatic.borg.pattern.Pattern(source_directory)
borgmatic.borg.pattern.Pattern(
source_directory, source=borgmatic.borg.pattern.Pattern_source.CONFIG
)
for source_directory in config.get('source_directories', ())
)
+ tuple(
@@ -144,6 +147,7 @@ def expand_patterns(patterns, working_directory=None, skip_paths=None):
pattern.type,
pattern.style,
pattern.device,
pattern.source,
)
for expanded_path in expand_directory(pattern.path, working_directory)
)
@@ -178,6 +182,7 @@ def device_map_patterns(patterns, working_directory=None):
and os.path.exists(full_path)
else None
),
source=pattern.source,
)
for pattern in patterns
for full_path in (os.path.join(working_directory or '', pattern.path),)
+1 -1
View File
@@ -61,7 +61,7 @@ def run_arbitrary_borg(
tuple(shlex.quote(part) for part in full_command),
output_file=DO_NOT_CAPTURE,
shell=True,
extra_environment=dict(
environment=dict(
(environment.make_environment(config) or {}),
**{
'BORG_REPO': repository_path,
+1 -1
View File
@@ -36,7 +36,7 @@ def break_lock(
execute_command(
full_command,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
+1 -1
View File
@@ -56,7 +56,7 @@ def change_passphrase(
full_command,
output_file=borgmatic.execute.DO_NOT_CAPTURE,
output_log_level=logging.ANSWER,
extra_environment=environment.make_environment(config_without_passphrase),
environment=environment.make_environment(config_without_passphrase),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
+1 -1
View File
@@ -182,7 +182,7 @@ def check_archives(
output_file=(
DO_NOT_CAPTURE if check_arguments.repair or check_arguments.progress else None
),
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
+1 -1
View File
@@ -49,7 +49,7 @@ def compact_segments(
execute_command(
full_command,
output_log_level=logging.INFO,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
+26 -13
View File
@@ -132,41 +132,53 @@ def collect_special_file_paths(
used.
Skip looking for special files in the given borgmatic runtime directory, as borgmatic creates
its own special files there for database dumps. And if the borgmatic runtime directory is
configured to be excluded from the files Borg backs up, error, because this means Borg won't be
able to consume any database dumps and therefore borgmatic will hang.
its own special files there for database dumps and we don't want those omitted.
Additionally, if the borgmatic runtime directory is not contained somewhere in the files Borg
plans to backup, that means the user must have excluded the runtime directory (e.g. via
"exclude_patterns" or similar). Therefore, raise, because this means Borg won't be able to
consume any database dumps and therefore borgmatic will hang when it tries to do so.
'''
# Omit "--exclude-nodump" from the Borg dry run command, because that flag causes Borg to open
# files including any named pipe we've created.
# files including any named pipe we've created. And omit "--filter" because that can break the
# paths output parsing below such that path lines no longer start with th expected "- ".
paths_output = execute_command_and_capture_output(
tuple(argument for argument in create_command if argument != '--exclude-nodump')
flags.omit_flag_and_value(flags.omit_flag(create_command, '--exclude-nodump'), '--filter')
+ ('--dry-run', '--list'),
capture_stderr=True,
working_directory=working_directory,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
# These are all the individual files that Borg is planning to backup as determined by the Borg
# create dry run above.
paths = tuple(
path_line.split(' ', 1)[1]
for path_line in paths_output.split('\n')
if path_line and path_line.startswith('- ') or path_line.startswith('+ ')
)
skip_paths = {}
# These are the subset of those files that contain the borgmatic runtime directory.
paths_containing_runtime_directory = {}
if os.path.exists(borgmatic_runtime_directory):
skip_paths = {
paths_containing_runtime_directory = {
path for path in paths if any_parent_directories(path, (borgmatic_runtime_directory,))
}
if not skip_paths and not dry_run:
# If no paths to backup contain the runtime directory, it must've been excluded.
if not paths_containing_runtime_directory and not dry_run:
raise ValueError(
f'The runtime directory {os.path.normpath(borgmatic_runtime_directory)} overlaps with the configured excludes or patterns with excludes. Please ensure the runtime directory is not excluded.'
)
return tuple(
path for path in paths if special_file(path, working_directory) if path not in skip_paths
path
for path in paths
if special_file(path, working_directory)
if path not in paths_containing_runtime_directory
)
@@ -325,6 +337,7 @@ def make_base_create_command(
special_file_path,
borgmatic.borg.pattern.Pattern_type.NO_RECURSE,
borgmatic.borg.pattern.Pattern_style.FNMATCH,
source=borgmatic.borg.pattern.Pattern_source.INTERNAL,
)
for special_file_path in special_file_paths
),
@@ -409,7 +422,7 @@ def create_archive(
output_log_level,
output_file,
working_directory=working_directory,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
@@ -417,7 +430,7 @@ def create_archive(
return execute_command_and_capture_output(
create_flags + create_positional_arguments,
working_directory=working_directory,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
@@ -427,7 +440,7 @@ def create_archive(
output_log_level,
output_file,
working_directory=working_directory,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
)
+1 -1
View File
@@ -128,7 +128,7 @@ def delete_archives(
borgmatic.execute.execute_command(
command,
output_log_level=logging.ANSWER,
extra_environment=borgmatic.borg.environment.make_environment(config),
environment=borgmatic.borg.environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
+32 -12
View File
@@ -10,13 +10,10 @@ OPTION_TO_ENVIRONMENT_VARIABLE = {
'borg_files_cache_ttl': 'BORG_FILES_CACHE_TTL',
'borg_security_directory': 'BORG_SECURITY_DIR',
'borg_keys_directory': 'BORG_KEYS_DIR',
'encryption_passphrase': 'BORG_PASSPHRASE',
'ssh_command': 'BORG_RSH',
'temporary_directory': 'TMPDIR',
}
CREDENTIAL_OPTIONS = {'encryption_passphrase'}
DEFAULT_BOOL_OPTION_TO_DOWNCASE_ENVIRONMENT_VARIABLE = {
'relocated_repo_access_is_ok': 'BORG_RELOCATED_REPO_ACCESS_IS_OK',
'unknown_unencrypted_repo_access_is_ok': 'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK',
@@ -29,27 +26,50 @@ DEFAULT_BOOL_OPTION_TO_UPPERCASE_ENVIRONMENT_VARIABLE = {
def make_environment(config):
'''
Given a borgmatic configuration dict, return its options converted to a Borg environment
variable dict.
Given a borgmatic configuration dict, convert it to a Borg environment variable dict, merge it
with a copy of the current environment variables, and return the result.
Do not reuse this environment across multiple Borg invocations, because it can include
references to resources like anonymous pipes for passphrases—which can only be consumed once.
Here's how native Borg precedence works for a few of the environment variables:
1. BORG_PASSPHRASE, if set, is used first.
2. BORG_PASSCOMMAND is used only if BORG_PASSPHRASE isn't set.
3. BORG_PASSPHRASE_FD is used only if neither of the above are set.
In borgmatic, we want to simulate this precedence order, but there are some additional
complications. First, values can come from either configuration or from environment variables
set outside borgmatic; configured options should take precedence. Second, when borgmatic gets a
passphrase—directly from configuration or indirectly via a credential hook or a passcommand—we
want to pass that passphrase to Borg via an anonymous pipe (+ BORG_PASSPHRASE_FD), since that's
more secure than using an environment variable (BORG_PASSPHRASE).
'''
environment = {}
environment = dict(os.environ)
for option_name, environment_variable_name in OPTION_TO_ENVIRONMENT_VARIABLE.items():
value = config.get(option_name)
if option_name in CREDENTIAL_OPTIONS and value is not None:
value = borgmatic.hooks.credential.parse.resolve_credential(value)
if value is not None:
environment[environment_variable_name] = str(value)
passphrase = borgmatic.borg.passcommand.get_passphrase_from_passcommand(config)
if 'encryption_passphrase' in config:
environment.pop('BORG_PASSPHRASE', None)
environment.pop('BORG_PASSCOMMAND', None)
# If the passcommand produced a passphrase, send it to Borg via an anonymous pipe.
if passphrase:
if 'encryption_passcommand' in config:
environment.pop('BORG_PASSCOMMAND', None)
passphrase = borgmatic.hooks.credential.parse.resolve_credential(
config.get('encryption_passphrase'), config
)
if passphrase is None:
passphrase = borgmatic.borg.passcommand.get_passphrase_from_passcommand(config)
# If there's a passphrase (from configuration, from a configured credential, or from a
# configured passcommand), send it to Borg via an anonymous pipe.
if passphrase is not None:
read_file_descriptor, write_file_descriptor = os.pipe()
os.write(write_file_descriptor, passphrase.encode('utf-8'))
os.close(write_file_descriptor)
+1 -1
View File
@@ -67,7 +67,7 @@ def export_key(
full_command,
output_file=output_file,
output_log_level=logging.ANSWER,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
+1 -1
View File
@@ -70,7 +70,7 @@ def export_tar_archive(
full_command,
output_file=DO_NOT_CAPTURE if destination_path == '-' else None,
output_log_level=output_log_level,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
+4 -4
View File
@@ -58,7 +58,7 @@ def extract_last_archive_dry_run(
execute_command(
full_extract_command,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
@@ -154,7 +154,7 @@ def extract_archive(
return execute_command(
full_command,
output_file=DO_NOT_CAPTURE,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=full_destination_path,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
@@ -166,7 +166,7 @@ def extract_archive(
full_command,
output_file=subprocess.PIPE,
run_to_completion=False,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=full_destination_path,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
@@ -176,7 +176,7 @@ def extract_archive(
# if the restore paths don't exist in the archive.
execute_command(
full_command,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=full_destination_path,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
+41
View File
@@ -156,3 +156,44 @@ def warn_for_aggressive_archive_flags(json_command, json_output):
logger.debug(f'Cannot parse JSON output from archive command: {error}')
except (TypeError, KeyError):
logger.debug('Cannot parse JSON output from archive command: No "archives" key found')
def omit_flag(arguments, flag):
'''
Given a sequence of Borg command-line arguments, return them with the given (valueless) flag
omitted. For instance, if the flag is "--flag" and arguments is:
('borg', 'create', '--flag', '--other-flag')
... then return:
('borg', 'create', '--other-flag')
'''
return tuple(argument for argument in arguments if argument != flag)
def omit_flag_and_value(arguments, flag):
'''
Given a sequence of Borg command-line arguments, return them with the given flag and its
corresponding value omitted. For instance, if the flag is "--flag" and arguments is:
('borg', 'create', '--flag', 'value', '--other-flag')
... or:
('borg', 'create', '--flag=value', '--other-flag')
... then return:
('borg', 'create', '--other-flag')
'''
# This works by zipping together a list of overlapping pairwise arguments. E.g., ('one', 'two',
# 'three', 'four') becomes ((None, 'one'), ('one, 'two'), ('two', 'three'), ('three', 'four')).
# This makes it easy to "look back" at the previous arguments so we can exclude both a flag and
# its value.
return tuple(
argument
for (previous_argument, argument) in zip((None,) + arguments, arguments)
if flag not in (previous_argument, argument)
if not argument.startswith(f'{flag}=')
)
+2 -2
View File
@@ -102,7 +102,7 @@ def display_archives_info(
json_info = execute_command_and_capture_output(
json_command,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
@@ -116,7 +116,7 @@ def display_archives_info(
execute_command(
main_command,
output_log_level=logging.ANSWER,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
+3 -3
View File
@@ -124,7 +124,7 @@ def capture_archive_listing(
local_path,
remote_path,
),
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
@@ -221,7 +221,7 @@ def list_archive(
local_path,
remote_path,
),
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
@@ -257,7 +257,7 @@ def list_archive(
execute_command(
main_command,
output_log_level=logging.ANSWER,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
+2 -2
View File
@@ -66,7 +66,7 @@ def mount_archive(
execute_command(
full_command,
output_file=DO_NOT_CAPTURE,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
@@ -75,7 +75,7 @@ def mount_archive(
execute_command(
full_command,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
+3 -13
View File
@@ -9,21 +9,14 @@ logger = logging.getLogger(__name__)
@functools.cache
def run_passcommand(passcommand, passphrase_configured, working_directory):
def run_passcommand(passcommand, working_directory):
'''
Run the given passcommand using the given working directory and return the passphrase produced
by the command. But bail first if a passphrase is already configured; this mimics Borg's
behavior.
by the command.
Cache the results so that the passcommand only needs to run—and potentially prompt the user—once
per borgmatic invocation.
'''
if passcommand and passphrase_configured:
logger.warning(
'Ignoring the "encryption_passcommand" option because "encryption_passphrase" is set'
)
return None
return borgmatic.execute.execute_command_and_capture_output(
shlex.split(passcommand),
working_directory=working_directory,
@@ -44,7 +37,4 @@ def get_passphrase_from_passcommand(config):
if not passcommand:
return None
passphrase = config.get('encryption_passphrase')
working_directory = borgmatic.config.paths.get_working_directory(config)
return run_passcommand(passcommand, bool(passphrase is not None), working_directory)
return run_passcommand(passcommand, borgmatic.config.paths.get_working_directory(config))
+20 -1
View File
@@ -20,12 +20,31 @@ class Pattern_style(enum.Enum):
PATH_FULL_MATCH = 'pf'
class Pattern_source(enum.Enum):
'''
Where the pattern came from within borgmatic. This is important because certain use cases (like
filesystem snapshotting) only want to consider patterns that the user actually put in a
configuration file and not patterns from other sources.
'''
# The pattern is from a borgmatic configuration option, e.g. listed in "source_directories".
CONFIG = 'config'
# The pattern is generated internally within borgmatic, e.g. for special file excludes.
INTERNAL = 'internal'
# The pattern originates from within a borgmatic hook, e.g. a database hook that adds its dump
# directory.
HOOK = 'hook'
Pattern = collections.namedtuple(
'Pattern',
('path', 'type', 'style', 'device'),
('path', 'type', 'style', 'device', 'source'),
defaults=(
Pattern_type.ROOT,
Pattern_style.NONE,
None,
Pattern_source.HOOK,
),
)
+1 -1
View File
@@ -96,7 +96,7 @@ def prune_archives(
execute_command(
full_command,
output_log_level=output_log_level,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
+1 -1
View File
@@ -98,7 +98,7 @@ def create_repository(
execute_command(
repo_create_command,
output_file=DO_NOT_CAPTURE,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
+1 -1
View File
@@ -88,7 +88,7 @@ def delete_repository(
if repo_delete_arguments.force or repo_delete_arguments.cache_only
else borgmatic.execute.DO_NOT_CAPTURE
),
extra_environment=borgmatic.borg.environment.make_environment(config),
environment=borgmatic.borg.environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
+2 -2
View File
@@ -56,7 +56,7 @@ def display_repository_info(
if repo_info_arguments.json:
return execute_command_and_capture_output(
full_command,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
@@ -65,7 +65,7 @@ def display_repository_info(
execute_command(
full_command,
output_log_level=logging.ANSWER,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
+3 -3
View File
@@ -49,7 +49,7 @@ def resolve_archive_name(
output = execute_command_and_capture_output(
full_command,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
@@ -164,7 +164,7 @@ def list_repository(
json_listing = execute_command_and_capture_output(
json_command,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
@@ -178,7 +178,7 @@ def list_repository(
execute_command(
main_command,
output_log_level=logging.ANSWER,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=working_directory,
borg_local_path=local_path,
borg_exit_codes=borg_exit_codes,
+1 -1
View File
@@ -57,7 +57,7 @@ def transfer_archives(
full_command,
output_log_level=logging.ANSWER,
output_file=DO_NOT_CAPTURE if transfer_arguments.progress else None,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
+1 -1
View File
@@ -21,7 +21,7 @@ def local_borg_version(config, local_path='borg'):
)
output = execute_command_and_capture_output(
full_command,
extra_environment=environment.make_environment(config),
environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
+22
View File
@@ -2402,3 +2402,25 @@ properties:
description: |
Configuration for integration with Linux LVM (Logical Volume
Manager).
container:
type: object
additionalProperties: false
properties:
secrets_directory:
type: string
description: |
Secrets directory to use instead of "/run/secrets".
example: /path/to/secrets
description: |
Configuration for integration with Docker or Podman secrets.
keepassxc:
type: object
additionalProperties: false
properties:
keepassxc_cli_command:
type: string
description: |
Command to use instead of "keepassxc-cli".
example: /usr/local/bin/keepassxc-cli
description: |
Configuration for integration with the KeePassXC password manager.
+47 -42
View File
@@ -1,7 +1,6 @@
import collections
import enum
import logging
import os
import select
import subprocess
import textwrap
@@ -243,6 +242,9 @@ def mask_command_secrets(full_command):
MAX_LOGGED_COMMAND_LENGTH = 1000
PREFIXES_OF_ENVIRONMENT_VARIABLES_TO_LOG = ('BORG_', 'PG', 'MARIADB_', 'MYSQL_')
def log_command(full_command, input_file=None, output_file=None, environment=None):
'''
Log the given command (a sequence of command/argument strings), along with its input/output file
@@ -251,7 +253,14 @@ def log_command(full_command, input_file=None, output_file=None, environment=Non
logger.debug(
textwrap.shorten(
' '.join(
tuple(f'{key}=***' for key in (environment or {}).keys())
tuple(
f'{key}=***'
for key in (environment or {}).keys()
if any(
key.startswith(prefix)
for prefix in PREFIXES_OF_ENVIRONMENT_VARIABLES_TO_LOG
)
)
+ mask_command_secrets(full_command)
),
width=MAX_LOGGED_COMMAND_LENGTH,
@@ -274,7 +283,7 @@ def execute_command(
output_file=None,
input_file=None,
shell=False,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path=None,
borg_exit_codes=None,
@@ -284,18 +293,17 @@ def execute_command(
Execute the given command (a sequence of command/argument strings) and log its output at the
given log level. If an open output file object is given, then write stdout to the file and only
log stderr. If an open input file object is given, then read stdin from the file. If shell is
True, execute the command within a shell. If an extra environment dict is given, then use it to
augment the current environment, and pass the result 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. If run to
completion is False, then return the process for the command without executing it to completion.
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. If run to completion is False, then return the process for
the command without executing it to completion.
Raise subprocesses.CalledProcessError if an error occurs while running the command.
'''
log_command(full_command, input_file, output_file, extra_environment)
environment = {**os.environ, **extra_environment} if extra_environment else None
log_command(full_command, input_file, output_file, environment)
do_not_capture = bool(output_file is DO_NOT_CAPTURE)
command = ' '.join(full_command) if shell else full_command
@@ -308,7 +316,7 @@ def execute_command(
env=environment,
cwd=working_directory,
# Necessary for the passcommand credential hook to work.
close_fds=not bool((extra_environment or {}).get('BORG_PASSPHRASE_FD')),
close_fds=not bool((environment or {}).get('BORG_PASSPHRASE_FD')),
)
if not run_to_completion:
return process
@@ -327,7 +335,7 @@ def execute_command_and_capture_output(
full_command,
capture_stderr=False,
shell=False,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path=None,
borg_exit_codes=None,
@@ -335,18 +343,16 @@ 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 extra environment dict is
given, then use it to augment the current environment, and pass the result 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.
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=extra_environment)
environment = {**os.environ, **extra_environment} if extra_environment else None
log_command(full_command, environment=environment)
command = ' '.join(full_command) if shell else full_command
try:
@@ -357,7 +363,7 @@ def execute_command_and_capture_output(
env=environment,
cwd=working_directory,
# Necessary for the passcommand credential hook to work.
close_fds=not bool((extra_environment or {}).get('BORG_PASSPHRASE_FD')),
close_fds=not bool((environment or {}).get('BORG_PASSPHRASE_FD')),
)
except subprocess.CalledProcessError as error:
if (
@@ -377,7 +383,7 @@ def execute_command_with_processes(
output_file=None,
input_file=None,
shell=False,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path=None,
borg_exit_codes=None,
@@ -391,19 +397,17 @@ def execute_command_with_processes(
If an open output file object is given, then write stdout to the file and only log stderr. But
if output log level is None, instead suppress logging and return the captured output for (only)
the given command. If an open input file object is given, then read stdin from the file. If
shell is True, execute the command within a shell. If an extra environment dict is given, then
use it to augment the current environment, and pass the result 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, then for any matching command or process (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.
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, then for any matching command
or process (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 or in the
upstream process.
'''
log_command(full_command, input_file, output_file, extra_environment)
environment = {**os.environ, **extra_environment} if extra_environment else None
log_command(full_command, input_file, output_file, environment)
do_not_capture = bool(output_file is DO_NOT_CAPTURE)
command = ' '.join(full_command) if shell else full_command
@@ -419,7 +423,7 @@ def execute_command_with_processes(
env=environment,
cwd=working_directory,
# Necessary for the passcommand credential hook to work.
close_fds=not bool((extra_environment or {}).get('BORG_PASSPHRASE_FD')),
close_fds=not bool((environment or {}).get('BORG_PASSPHRASE_FD')),
)
except (subprocess.CalledProcessError, OSError):
# Something has gone wrong. So vent each process' output buffer to prevent it from hanging.
@@ -430,13 +434,14 @@ def execute_command_with_processes(
process.kill()
raise
captured_outputs = log_outputs(
tuple(processes) + (command_process,),
(input_file, output_file),
output_log_level,
borg_local_path,
borg_exit_codes,
)
with borgmatic.logger.Log_prefix(None): # Log command output without any prefix.
captured_outputs = log_outputs(
tuple(processes) + (command_process,),
(input_file, output_file),
output_log_level,
borg_local_path,
borg_exit_codes,
)
if output_log_level is None:
return captured_outputs.get(command_process)
+8 -6
View File
@@ -30,16 +30,18 @@ def interpolate_context(hook_description, command, context):
def make_environment(current_environment, sys_module=sys):
'''
Given the existing system environment as a map from environment variable name to value, return
(in the same form) any extra environment variables that should be used when running command
hooks.
Given the existing system environment as a map from environment variable name to value, return a
copy of it, augmented with any extra environment variables that should be used when running
command hooks.
'''
environment = dict(current_environment)
# Detect whether we're running within a PyInstaller bundle. If so, set or clear LD_LIBRARY_PATH
# based on the value of LD_LIBRARY_PATH_ORIG. This prevents library version information errors.
if getattr(sys_module, 'frozen', False) and hasattr(sys_module, '_MEIPASS'):
return {'LD_LIBRARY_PATH': current_environment.get('LD_LIBRARY_PATH_ORIG', '')}
environment['LD_LIBRARY_PATH'] = environment.get('LD_LIBRARY_PATH_ORIG', '')
return {}
return environment
def execute_hook(commands, umask, config_filename, description, dry_run, **context):
@@ -85,7 +87,7 @@ def execute_hook(commands, umask, config_filename, description, dry_run, **conte
[command],
output_log_level=(logging.ERROR if description == 'on-error' else logging.WARNING),
shell=True,
extra_environment=make_environment(os.environ),
environment=make_environment(os.environ),
)
finally:
if original_umask:
+41
View File
@@ -0,0 +1,41 @@
import logging
import os
import re
logger = logging.getLogger(__name__)
SECRET_NAME_PATTERN = re.compile(r'^\w+$')
DEFAULT_SECRETS_DIRECTORY = '/run/secrets'
def load_credential(hook_config, config, credential_parameters):
'''
Given the hook configuration dict, the configuration dict, and a credential parameters tuple
containing a secret name to load, read the secret from the corresponding container secrets file
and return it.
Raise ValueError if the credential parameters is not one element, the secret name is invalid, or
the secret file cannot be read.
'''
try:
(secret_name,) = credential_parameters
except ValueError:
raise ValueError(f'Cannot load invalid secret name: "{' '.join(credential_parameters)}"')
if not SECRET_NAME_PATTERN.match(secret_name):
raise ValueError(f'Cannot load invalid secret name: "{secret_name}"')
try:
with open(
os.path.join(
config.get('working_directory', ''),
(hook_config or {}).get('secrets_directory', DEFAULT_SECRETS_DIRECTORY),
secret_name,
)
) as secret_file:
return secret_file.read().rstrip(os.linesep)
except (FileNotFoundError, OSError) as error:
logger.warning(error)
raise ValueError(f'Cannot load secret "{secret_name}" from file: {error.filename}')
+28
View File
@@ -0,0 +1,28 @@
import logging
import os
logger = logging.getLogger(__name__)
def load_credential(hook_config, config, credential_parameters):
'''
Given the hook configuration dict, the configuration dict, and a credential parameters tuple
containing a credential path to load, load the credential from file and return it.
Raise ValueError if the credential parameters is not one element or the secret file cannot be
read.
'''
try:
(credential_path,) = credential_parameters
except ValueError:
raise ValueError(f'Cannot load invalid credential: "{' '.join(credential_parameters)}"')
try:
with open(
os.path.join(config.get('working_directory', ''), credential_path)
) as credential_file:
return credential_file.read().rstrip(os.linesep)
except (FileNotFoundError, OSError) as error:
logger.warning(error)
raise ValueError(f'Cannot load credential file: {error.filename}')
+40
View File
@@ -0,0 +1,40 @@
import logging
import os
import shlex
import borgmatic.execute
logger = logging.getLogger(__name__)
def load_credential(hook_config, config, credential_parameters):
'''
Given the hook configuration dict, the configuration dict, and a credential parameters tuple
containing a KeePassXC database path and an attribute name to load, run keepassxc-cli to fetch
the corresponidng KeePassXC credential and return it.
Raise ValueError if keepassxc-cli can't retrieve the credential.
'''
try:
(database_path, attribute_name) = credential_parameters
except ValueError:
raise ValueError(
f'Cannot load credential with invalid KeePassXC database path and attribute name: "{' '.join(credential_parameters)}"'
)
if not os.path.exists(database_path):
raise ValueError(
f'Cannot load credential because KeePassXC database path does not exist: {database_path}'
)
return borgmatic.execute.execute_command_and_capture_output(
tuple(shlex.split((hook_config or {}).get('keepassxc_cli_command', 'keepassxc-cli')))
+ (
'show',
'--show-protected',
'--attributes',
'Password',
database_path,
attribute_name,
)
).rstrip(os.linesep)
+105 -23
View File
@@ -1,42 +1,124 @@
import functools
import re
import shlex
import borgmatic.hooks.dispatch
IS_A_HOOK = False
CREDENTIAL_PATTERN = re.compile(
r'\{credential +(?P<hook_name>[A-Za-z0-9_]+) +(?P<credential_name>[A-Za-z0-9_]+)\}'
)
GENERAL_CREDENTIAL_PATTERN = re.compile(r'\{credential( +[^}]*)?\}')
@functools.cache
def resolve_credential(value):
class Hash_adapter:
'''
Given a configuration value containing a string like "{credential hookname credentialname}", resolve it by
calling the relevant hook to get the actual credential value. If the given value does not
actually contain a credential tag, then return it unchanged.
A Hash_adapter instance wraps an unhashable object and pretends it's hashable. This is intended
for passing to a @functools.cache-decorated function to prevent it from complaining that an
argument is unhashable. It should only be used for arguments that you don't want to actually
impact the cache hashing, because Hash_adapter doesn't actually hash the object's contents.
Cache the value so repeated calls to this function don't need to load the credential repeatedly.
Example usage:
@functools.cache
def func(a, b):
print(a, b.actual_value)
return a
func(5, Hash_adapter({1: 2, 3: 4})) # Calls func(), prints, and returns.
func(5, Hash_adapter({1: 2, 3: 4})) # Hits the cache and just returns the value.
func(5, Hash_adapter({5: 6, 7: 8})) # Also uses cache, since the Hash_adapter is ignored.
In the above function, the "b" value is one that has been wrapped with Hash_adappter, and
therefore "b.actual_value" is necessary to access the original value.
'''
def __init__(self, actual_value):
self.actual_value = actual_value
def __eq__(self, other):
return True
def __hash__(self):
return 0
UNHASHABLE_TYPES = (dict, list, set)
def cache_ignoring_unhashable_arguments(function):
'''
A function decorator that caches calls to the decorated function but ignores any unhashable
arguments when performing cache lookups. This is intended to be a drop-in replacement for
functools.cache.
Example usage:
@cache_ignoring_unhashable_arguments
def func(a, b):
print(a, b)
return a
func(5, {1: 2, 3: 4}) # Calls func(), prints, and returns.
func(5, {1: 2, 3: 4}) # Hits the cache and just returns the value.
func(5, {5: 6, 7: 8}) # Also uses cache, since the unhashable value (the dict) is ignored.
'''
@functools.cache
def cached_function(*args, **kwargs):
return function(
*(arg.actual_value if isinstance(arg, Hash_adapter) else arg for arg in args),
**{
key: value.actual_value if isinstance(value, Hash_adapter) else value
for (key, value) in kwargs.items()
},
)
@functools.wraps(function)
def wrapper_function(*args, **kwargs):
return cached_function(
*(Hash_adapter(arg) if isinstance(arg, UNHASHABLE_TYPES) else arg for arg in args),
**{
key: Hash_adapter(value) if isinstance(value, UNHASHABLE_TYPES) else value
for (key, value) in kwargs.items()
},
)
wrapper_function.cache_clear = cached_function.cache_clear
return wrapper_function
CREDENTIAL_PATTERN = re.compile(r'\{credential( +(?P<hook_and_parameters>.*))?\}')
@cache_ignoring_unhashable_arguments
def resolve_credential(value, config):
'''
Given a configuration value containing a string like "{credential hookname credentialname}" and
a configuration dict, resolve the credential by calling the relevant hook to get the actual
credential value. If the given value does not actually contain a credential tag, then return it
unchanged.
Cache the value (ignoring the config for purposes of caching), so repeated calls to this
function don't need to load the credential repeatedly.
Raise ValueError if the config could not be parsed or the credential could not be loaded.
'''
if value is None:
return value
result = CREDENTIAL_PATTERN.sub(
lambda matcher: borgmatic.hooks.dispatch.call_hook(
'load_credential', {}, matcher.group('hook_name'), matcher.group('credential_name')
),
value,
)
matcher = CREDENTIAL_PATTERN.match(value)
# If we've tried to parse the credential, but the parsed result still looks kind of like a
# credential, it means it's invalid syntax.
if GENERAL_CREDENTIAL_PATTERN.match(result):
if not matcher:
return value
hook_and_parameters = matcher.group('hook_and_parameters')
if not hook_and_parameters:
raise ValueError(f'Cannot load credential with invalid syntax "{value}"')
return result
(hook_name, *credential_parameters) = shlex.split(hook_and_parameters)
if not credential_parameters:
raise ValueError(f'Cannot load credential with invalid syntax "{value}"')
return borgmatic.hooks.dispatch.call_hook(
'load_credential', config, hook_name, tuple(credential_parameters)
)
+11 -3
View File
@@ -8,14 +8,22 @@ logger = logging.getLogger(__name__)
CREDENTIAL_NAME_PATTERN = re.compile(r'^\w+$')
def load_credential(hook_config, config, credential_name):
def load_credential(hook_config, config, credential_parameters):
'''
Given the hook configuration dict, the configuration dict, and a credential name to load, read
the credential from the corresponding systemd credential file and return it.
Given the hook configuration dict, the configuration dict, and a credential parameters tuple
containing a credential name to load, read the credential from the corresponding systemd
credential file and return it.
Raise ValueError if the systemd CREDENTIALS_DIRECTORY environment variable is not set, the
credential name is invalid, or the credential file cannot be read.
'''
try:
(credential_name,) = credential_parameters
except ValueError:
raise ValueError(
f'Cannot load invalid credential name: "{' '.join(credential_parameters)}"'
)
credentials_directory = os.environ.get('CREDENTIALS_DIRECTORY')
if not credentials_directory:
+10 -2
View File
@@ -55,9 +55,17 @@ def dump_data_sources(
manifest_file,
)
patterns.extend(borgmatic.borg.pattern.Pattern(config_path) for config_path in config_paths)
patterns.extend(
borgmatic.borg.pattern.Pattern(
config_path, source=borgmatic.borg.pattern.Pattern_source.HOOK
)
for config_path in config_paths
)
patterns.append(
borgmatic.borg.pattern.Pattern(os.path.join(borgmatic_runtime_directory, 'bootstrap'))
borgmatic.borg.pattern.Pattern(
os.path.join(borgmatic_runtime_directory, 'bootstrap'),
source=borgmatic.borg.pattern.Pattern_source.HOOK,
)
)
return []
+19 -6
View File
@@ -54,7 +54,9 @@ def get_subvolumes(btrfs_command, findmnt_command, patterns=None):
between the current Btrfs filesystem and subvolume mount points and the paths of any patterns.
The idea is that these pattern paths represent the requested subvolumes to snapshot.
If patterns is None, then return all subvolumes, sorted by path.
Only include subvolumes that contain at least one root pattern sourced from borgmatic
configuration (as opposed to generated elsewhere in borgmatic). But if patterns is None, then
return all subvolumes instead, sorted by path.
Return the result as a sequence of matching subvolume mount points.
'''
@@ -73,7 +75,12 @@ def get_subvolumes(btrfs_command, findmnt_command, patterns=None):
mount_point, candidate_patterns
),
)
if patterns is None or contained_patterns
if patterns is None
or any(
pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT
and pattern.source == borgmatic.borg.pattern.Pattern_source.CONFIG
for pattern in contained_patterns
)
)
return tuple(sorted(subvolumes, key=lambda subvolume: subvolume.path))
@@ -121,6 +128,7 @@ def make_snapshot_exclude_pattern(subvolume_path): # pragma: no cover
),
borgmatic.borg.pattern.Pattern_type.NO_RECURSE,
borgmatic.borg.pattern.Pattern_style.FNMATCH,
source=borgmatic.borg.pattern.Pattern_source.HOOK,
)
@@ -153,6 +161,7 @@ def make_borg_snapshot_pattern(subvolume_path, pattern):
pattern.type,
pattern.style,
pattern.device,
source=borgmatic.borg.pattern.Pattern_source.HOOK,
)
@@ -198,7 +207,8 @@ def dump_data_sources(
dry_run_label = ' (dry run; not actually snapshotting anything)' if dry_run else ''
logger.info(f'Snapshotting Btrfs subvolumes{dry_run_label}')
# Based on the configured patterns, determine Btrfs subvolumes to backup.
# Based on the configured patterns, determine Btrfs subvolumes to backup. Only consider those
# patterns that came from actual user configuration (as opposed to, say, other hooks).
btrfs_command = hook_config.get('btrfs_command', 'btrfs')
findmnt_command = hook_config.get('findmnt_command', 'findmnt')
subvolumes = get_subvolumes(btrfs_command, findmnt_command, patterns)
@@ -299,9 +309,12 @@ def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, d
logger.debug(error)
return
# Strip off the subvolume path from the end of the snapshot path and then delete the
# resulting directory.
shutil.rmtree(snapshot_path.rsplit(subvolume.path, 1)[0])
# Remove the snapshot parent directory if it still exists. (It might not exist if the
# snapshot was for "/".)
snapshot_parent_dir = snapshot_path.rsplit(subvolume.path, 1)[0]
if os.path.isdir(snapshot_parent_dir):
shutil.rmtree(snapshot_parent_dir)
def make_data_source_dump_patterns(
+39 -9
View File
@@ -1,5 +1,6 @@
import collections
import glob
import hashlib
import json
import logging
import os
@@ -33,7 +34,9 @@ def get_logical_volumes(lsblk_command, patterns=None):
between the current LVM logical volume mount points and the paths of any patterns. The idea is
that these pattern paths represent the requested logical volumes to snapshot.
If patterns is None, include all logical volume mounts points, not just those in patterns.
Only include logical volumes that contain at least one root pattern sourced from borgmatic
configuration (as opposed to generated elsewhere in borgmatic). But if patterns is None, include
all logical volume mounts points instead, not just those in patterns.
Return the result as a sequence of Logical_volume instances.
'''
@@ -72,7 +75,12 @@ def get_logical_volumes(lsblk_command, patterns=None):
device['mountpoint'], candidate_patterns
),
)
if not patterns or contained_patterns
if not patterns
or any(
pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT
and pattern.source == borgmatic.borg.pattern.Pattern_source.CONFIG
for pattern in contained_patterns
)
)
except KeyError as error:
raise ValueError(f'Invalid {lsblk_command} output: Missing key "{error}"')
@@ -124,10 +132,14 @@ def mount_snapshot(mount_command, snapshot_device, snapshot_mount_path): # prag
)
def make_borg_snapshot_pattern(pattern, normalized_runtime_directory):
MOUNT_POINT_HASH_LENGTH = 10
def make_borg_snapshot_pattern(pattern, logical_volume, normalized_runtime_directory):
'''
Given a Borg pattern as a borgmatic.borg.pattern.Pattern instance, return a new Pattern with its
path rewritten to be in a snapshot directory based on the given runtime directory.
Given a Borg pattern as a borgmatic.borg.pattern.Pattern instance and a Logical_volume
containing it, return a new Pattern with its path rewritten to be in a snapshot directory based
on both the given runtime directory and the given Logical_volume's mount point.
Move any initial caret in a regular expression pattern path to the beginning, so as not to break
the regular expression.
@@ -142,6 +154,13 @@ def make_borg_snapshot_pattern(pattern, normalized_runtime_directory):
rewritten_path = initial_caret + os.path.join(
normalized_runtime_directory,
'lvm_snapshots',
# Including this hash prevents conflicts between snapshot patterns for different logical
# volumes. For instance, without this, snapshotting a logical volume at /var and another at
# /var/spool would result in overlapping snapshot patterns and therefore colliding mount
# attempts.
hashlib.shake_256(logical_volume.mount_point.encode('utf-8')).hexdigest(
MOUNT_POINT_HASH_LENGTH
),
'.', # Borg 1.4+ "slashdot" hack.
# Included so that the source directory ends up in the Borg archive at its "original" path.
pattern.path.lstrip('^').lstrip(os.path.sep),
@@ -152,6 +171,7 @@ def make_borg_snapshot_pattern(pattern, normalized_runtime_directory):
pattern.type,
pattern.style,
pattern.device,
source=borgmatic.borg.pattern.Pattern_source.HOOK,
)
@@ -180,7 +200,8 @@ def dump_data_sources(
dry_run_label = ' (dry run; not actually snapshotting anything)' if dry_run else ''
logger.info(f'Snapshotting LVM logical volumes{dry_run_label}')
# List logical volumes to get their mount points.
# List logical volumes to get their mount points, but only consider those patterns that came
# from actual user configuration (as opposed to, say, other hooks).
lsblk_command = hook_config.get('lsblk_command', 'lsblk')
requested_logical_volumes = get_logical_volumes(lsblk_command, patterns)
@@ -218,6 +239,9 @@ def dump_data_sources(
snapshot_mount_path = os.path.join(
normalized_runtime_directory,
'lvm_snapshots',
hashlib.shake_256(logical_volume.mount_point.encode('utf-8')).hexdigest(
MOUNT_POINT_HASH_LENGTH
),
logical_volume.mount_point.lstrip(os.path.sep),
)
@@ -233,7 +257,9 @@ def dump_data_sources(
)
for pattern in logical_volume.contained_patterns:
snapshot_pattern = make_borg_snapshot_pattern(pattern, normalized_runtime_directory)
snapshot_pattern = make_borg_snapshot_pattern(
pattern, logical_volume, normalized_runtime_directory
)
# Attempt to update the pattern in place, since pattern order matters to Borg.
try:
@@ -337,6 +363,7 @@ def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, d
os.path.normpath(borgmatic_runtime_directory),
),
'lvm_snapshots',
'*',
)
logger.debug(f'Looking for snapshots to remove in {snapshots_glob}{dry_run_label}')
umount_command = hook_config.get('umount_command', 'umount')
@@ -349,7 +376,10 @@ def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, d
snapshot_mount_path = os.path.join(
snapshots_directory, logical_volume.mount_point.lstrip(os.path.sep)
)
if not os.path.isdir(snapshot_mount_path):
# If the snapshot mount path is empty, this is probably just a "shadow" of a nested
# logical volume and therefore there's nothing to unmount.
if not os.path.isdir(snapshot_mount_path) or not os.listdir(snapshot_mount_path):
continue
# This might fail if the directory is already mounted, but we swallow errors here since
@@ -374,7 +404,7 @@ def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, d
return
except subprocess.CalledProcessError as error:
logger.debug(error)
return
continue
if not dry_run:
shutil.rmtree(snapshots_directory)
+45 -25
View File
@@ -26,11 +26,11 @@ def make_dump_path(base_directory): # pragma: no cover
SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys')
def database_names_to_dump(database, extra_environment, dry_run):
def database_names_to_dump(database, config, environment, dry_run):
'''
Given a requested database config, return the corresponding sequence of database names to dump.
In the case of "all", query for the names of databases on the configured host and return them,
excluding any system databases that will cause problems during restore.
Given a requested database config and a configuration dict, return the corresponding sequence of
database names to dump. In the case of "all", query for the names of databases on the configured
host and return them, excluding any system databases that will cause problems during restore.
'''
if database['name'] != 'all':
return (database['name'],)
@@ -47,7 +47,10 @@ def database_names_to_dump(database, extra_environment, dry_run):
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
+ (
('--user', borgmatic.hooks.credential.parse.resolve_credential(database['username']))
(
'--user',
borgmatic.hooks.credential.parse.resolve_credential(database['username'], config),
)
if 'username' in database
else ()
)
@@ -55,9 +58,7 @@ def database_names_to_dump(database, extra_environment, dry_run):
+ ('--execute', 'show schemas')
)
logger.debug('Querying for "all" MariaDB databases to dump')
show_output = execute_command_and_capture_output(
show_command, extra_environment=extra_environment
)
show_output = execute_command_and_capture_output(show_command, environment=environment)
return tuple(
show_name
@@ -67,7 +68,7 @@ def database_names_to_dump(database, extra_environment, dry_run):
def execute_dump_command(
database, dump_path, database_names, extra_environment, dry_run, dry_run_label
database, config, dump_path, database_names, environment, dry_run, dry_run_label
):
'''
Kick off a dump for the given MariaDB database (provided as a configuration dict) to a named
@@ -102,7 +103,10 @@ def execute_dump_command(
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
+ (
('--user', borgmatic.hooks.credential.parse.resolve_credential(database['username']))
(
'--user',
borgmatic.hooks.credential.parse.resolve_credential(database['username'], config),
)
if 'username' in database
else ()
)
@@ -119,7 +123,7 @@ def execute_dump_command(
return execute_command(
dump_command,
extra_environment=extra_environment,
environment=environment,
run_to_completion=False,
)
@@ -161,12 +165,19 @@ def dump_data_sources(
for database in databases:
dump_path = make_dump_path(borgmatic_runtime_directory)
extra_environment = (
{'MYSQL_PWD': borgmatic.hooks.credential.parse.resolve_credential(database['password'])}
if 'password' in database
else None
environment = dict(
os.environ,
**(
{
'MYSQL_PWD': borgmatic.hooks.credential.parse.resolve_credential(
database['password'], config
)
}
if 'password' in database
else {}
),
)
dump_database_names = database_names_to_dump(database, extra_environment, dry_run)
dump_database_names = database_names_to_dump(database, config, environment, dry_run)
if not dump_database_names:
if dry_run:
@@ -181,9 +192,10 @@ def dump_data_sources(
processes.append(
execute_dump_command(
renamed_database,
config,
dump_path,
(dump_name,),
extra_environment,
environment,
dry_run,
dry_run_label,
)
@@ -192,9 +204,10 @@ def dump_data_sources(
processes.append(
execute_dump_command(
database,
config,
dump_path,
dump_database_names,
extra_environment,
environment,
dry_run,
dry_run_label,
)
@@ -203,7 +216,8 @@ def dump_data_sources(
if not dry_run:
patterns.append(
borgmatic.borg.pattern.Pattern(
os.path.join(borgmatic_runtime_directory, 'mariadb_databases')
os.path.join(borgmatic_runtime_directory, 'mariadb_databases'),
source=borgmatic.borg.pattern.Pattern_source.HOOK,
)
)
@@ -265,12 +279,18 @@ def restore_data_source_dump(
connection_params['port'] or data_source.get('restore_port', data_source.get('port', ''))
)
username = borgmatic.hooks.credential.parse.resolve_credential(
connection_params['username']
or data_source.get('restore_username', data_source.get('username'))
(
connection_params['username']
or data_source.get('restore_username', data_source.get('username'))
),
config,
)
password = borgmatic.hooks.credential.parse.resolve_credential(
connection_params['password']
or data_source.get('restore_password', data_source.get('password'))
(
connection_params['password']
or data_source.get('restore_password', data_source.get('password'))
),
config,
)
mariadb_restore_command = tuple(
@@ -289,7 +309,7 @@ def restore_data_source_dump(
+ (('--protocol', 'tcp') if hostname or port else ())
+ (('--user', username) if username else ())
)
extra_environment = {'MYSQL_PWD': password} if password else None
environment = dict(os.environ, **({'MYSQL_PWD': password} if password else {}))
logger.debug(f"Restoring MariaDB database {data_source['name']}{dry_run_label}")
if dry_run:
@@ -302,5 +322,5 @@ def restore_data_source_dump(
[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment=extra_environment,
environment=environment,
)
+22 -9
View File
@@ -69,7 +69,7 @@ def dump_data_sources(
if dry_run:
continue
command = build_dump_command(database, dump_filename, dump_format)
command = build_dump_command(database, config, dump_filename, dump_format)
if dump_format == 'directory':
dump.create_parent_directory_for_dump(dump_filename)
@@ -81,14 +81,15 @@ def dump_data_sources(
if not dry_run:
patterns.append(
borgmatic.borg.pattern.Pattern(
os.path.join(borgmatic_runtime_directory, 'mongodb_databases')
os.path.join(borgmatic_runtime_directory, 'mongodb_databases'),
source=borgmatic.borg.pattern.Pattern_source.HOOK,
)
)
return processes
def build_dump_command(database, dump_filename, dump_format):
def build_dump_command(database, config, dump_filename, dump_format):
'''
Return the mongodump command from a single database configuration.
'''
@@ -103,7 +104,9 @@ def build_dump_command(database, dump_filename, dump_format):
(
'--username',
shlex.quote(
borgmatic.hooks.credential.parse.resolve_credential(database['username'])
borgmatic.hooks.credential.parse.resolve_credential(
database['username'], config
)
),
)
if 'username' in database
@@ -113,7 +116,9 @@ def build_dump_command(database, dump_filename, dump_format):
(
'--password',
shlex.quote(
borgmatic.hooks.credential.parse.resolve_credential(database['password'])
borgmatic.hooks.credential.parse.resolve_credential(
database['password'], config
)
),
)
if 'password' in database
@@ -192,7 +197,7 @@ def restore_data_source_dump(
data_source.get('hostname'),
)
restore_command = build_restore_command(
extract_process, data_source, dump_filename, connection_params
extract_process, data_source, config, dump_filename, connection_params
)
logger.debug(f"Restoring MongoDB database {data_source['name']}{dry_run_label}")
@@ -209,7 +214,7 @@ def restore_data_source_dump(
)
def build_restore_command(extract_process, database, dump_filename, connection_params):
def build_restore_command(extract_process, database, config, dump_filename, connection_params):
'''
Return the mongorestore command from a single database configuration.
'''
@@ -218,10 +223,18 @@ def build_restore_command(extract_process, database, dump_filename, connection_p
)
port = str(connection_params['port'] or database.get('restore_port', database.get('port', '')))
username = borgmatic.hooks.credential.parse.resolve_credential(
connection_params['username'] or database.get('restore_username', database.get('username'))
(
connection_params['username']
or database.get('restore_username', database.get('username'))
),
config,
)
password = borgmatic.hooks.credential.parse.resolve_credential(
connection_params['password'] or database.get('restore_password', database.get('password'))
(
connection_params['password']
or database.get('restore_password', database.get('password'))
),
config,
)
command = ['mongorestore']
+45 -25
View File
@@ -26,11 +26,11 @@ def make_dump_path(base_directory): # pragma: no cover
SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys')
def database_names_to_dump(database, extra_environment, dry_run):
def database_names_to_dump(database, config, environment, dry_run):
'''
Given a requested database config, return the corresponding sequence of database names to dump.
In the case of "all", query for the names of databases on the configured host and return them,
excluding any system databases that will cause problems during restore.
Given a requested database config and a configuration dict, return the corresponding sequence of
database names to dump. In the case of "all", query for the names of databases on the configured
host and return them, excluding any system databases that will cause problems during restore.
'''
if database['name'] != 'all':
return (database['name'],)
@@ -47,7 +47,10 @@ def database_names_to_dump(database, extra_environment, dry_run):
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
+ (
('--user', borgmatic.hooks.credential.parse.resolve_credential(database['username']))
(
'--user',
borgmatic.hooks.credential.parse.resolve_credential(database['username'], config),
)
if 'username' in database
else ()
)
@@ -55,9 +58,7 @@ def database_names_to_dump(database, extra_environment, dry_run):
+ ('--execute', 'show schemas')
)
logger.debug('Querying for "all" MySQL databases to dump')
show_output = execute_command_and_capture_output(
show_command, extra_environment=extra_environment
)
show_output = execute_command_and_capture_output(show_command, environment=environment)
return tuple(
show_name
@@ -67,7 +68,7 @@ def database_names_to_dump(database, extra_environment, dry_run):
def execute_dump_command(
database, dump_path, database_names, extra_environment, dry_run, dry_run_label
database, config, dump_path, database_names, environment, dry_run, dry_run_label
):
'''
Kick off a dump for the given MySQL/MariaDB database (provided as a configuration dict) to a
@@ -101,7 +102,10 @@ def execute_dump_command(
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
+ (
('--user', borgmatic.hooks.credential.parse.resolve_credential(database['username']))
(
'--user',
borgmatic.hooks.credential.parse.resolve_credential(database['username'], config),
)
if 'username' in database
else ()
)
@@ -118,7 +122,7 @@ def execute_dump_command(
return execute_command(
dump_command,
extra_environment=extra_environment,
environment=environment,
run_to_completion=False,
)
@@ -160,12 +164,19 @@ def dump_data_sources(
for database in databases:
dump_path = make_dump_path(borgmatic_runtime_directory)
extra_environment = (
{'MYSQL_PWD': borgmatic.hooks.credential.parse.resolve_credential(database['password'])}
if 'password' in database
else None
environment = dict(
os.environ,
**(
{
'MYSQL_PWD': borgmatic.hooks.credential.parse.resolve_credential(
database['password'], config
)
}
if 'password' in database
else {}
),
)
dump_database_names = database_names_to_dump(database, extra_environment, dry_run)
dump_database_names = database_names_to_dump(database, config, environment, dry_run)
if not dump_database_names:
if dry_run:
@@ -180,9 +191,10 @@ def dump_data_sources(
processes.append(
execute_dump_command(
renamed_database,
config,
dump_path,
(dump_name,),
extra_environment,
environment,
dry_run,
dry_run_label,
)
@@ -191,9 +203,10 @@ def dump_data_sources(
processes.append(
execute_dump_command(
database,
config,
dump_path,
dump_database_names,
extra_environment,
environment,
dry_run,
dry_run_label,
)
@@ -202,7 +215,8 @@ def dump_data_sources(
if not dry_run:
patterns.append(
borgmatic.borg.pattern.Pattern(
os.path.join(borgmatic_runtime_directory, 'mysql_databases')
os.path.join(borgmatic_runtime_directory, 'mysql_databases'),
source=borgmatic.borg.pattern.Pattern_source.HOOK,
)
)
@@ -264,12 +278,18 @@ def restore_data_source_dump(
connection_params['port'] or data_source.get('restore_port', data_source.get('port', ''))
)
username = borgmatic.hooks.credential.parse.resolve_credential(
connection_params['username']
or data_source.get('restore_username', data_source.get('username'))
(
connection_params['username']
or data_source.get('restore_username', data_source.get('username'))
),
config,
)
password = borgmatic.hooks.credential.parse.resolve_credential(
connection_params['password']
or data_source.get('restore_password', data_source.get('password'))
(
connection_params['password']
or data_source.get('restore_password', data_source.get('password'))
),
config,
)
mysql_restore_command = tuple(
@@ -288,7 +308,7 @@ def restore_data_source_dump(
+ (('--protocol', 'tcp') if hostname or port else ())
+ (('--user', username) if username else ())
)
extra_environment = {'MYSQL_PWD': password} if password else None
environment = dict(os.environ, **({'MYSQL_PWD': password} if password else {}))
logger.debug(f"Restoring MySQL database {data_source['name']}{dry_run_label}")
if dry_run:
@@ -301,5 +321,5 @@ def restore_data_source_dump(
[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment=extra_environment,
environment=environment,
)
+40 -37
View File
@@ -25,49 +25,52 @@ def make_dump_path(base_directory): # pragma: no cover
return dump.make_data_source_dump_path(base_directory, 'postgresql_databases')
def make_extra_environment(database, restore_connection_params=None):
def make_environment(database, config, restore_connection_params=None):
'''
Make the extra_environment dict from the given database configuration. If restore connection
params are given, this is for a restore operation.
Make an environment dict from the current environment variables and the given database
configuration. If restore connection params are given, this is for a restore operation.
'''
extra = dict()
environment = dict(os.environ)
try:
if restore_connection_params:
extra['PGPASSWORD'] = borgmatic.hooks.credential.parse.resolve_credential(
restore_connection_params.get('password')
or database.get('restore_password', database['password'])
environment['PGPASSWORD'] = borgmatic.hooks.credential.parse.resolve_credential(
(
restore_connection_params.get('password')
or database.get('restore_password', database['password'])
),
config,
)
else:
extra['PGPASSWORD'] = borgmatic.hooks.credential.parse.resolve_credential(
database['password']
environment['PGPASSWORD'] = borgmatic.hooks.credential.parse.resolve_credential(
database['password'], config
)
except (AttributeError, KeyError):
pass
if 'ssl_mode' in database:
extra['PGSSLMODE'] = database['ssl_mode']
environment['PGSSLMODE'] = database['ssl_mode']
if 'ssl_cert' in database:
extra['PGSSLCERT'] = database['ssl_cert']
environment['PGSSLCERT'] = database['ssl_cert']
if 'ssl_key' in database:
extra['PGSSLKEY'] = database['ssl_key']
environment['PGSSLKEY'] = database['ssl_key']
if 'ssl_root_cert' in database:
extra['PGSSLROOTCERT'] = database['ssl_root_cert']
environment['PGSSLROOTCERT'] = database['ssl_root_cert']
if 'ssl_crl' in database:
extra['PGSSLCRL'] = database['ssl_crl']
environment['PGSSLCRL'] = database['ssl_crl']
return extra
return environment
EXCLUDED_DATABASE_NAMES = ('template0', 'template1')
def database_names_to_dump(database, extra_environment, dry_run):
def database_names_to_dump(database, config, environment, dry_run):
'''
Given a requested database config, return the corresponding sequence of database names to dump.
In the case of "all" when a database format is given, query for the names of databases on the
configured host and return them. For "all" without a database format, just return a sequence
containing "all".
Given a requested database config and a configuration dict, return the corresponding sequence of
database names to dump. In the case of "all" when a database format is given, query for the
names of databases on the configured host and return them. For "all" without a database format,
just return a sequence containing "all".
'''
requested_name = database['name']
@@ -89,7 +92,7 @@ def database_names_to_dump(database, extra_environment, dry_run):
+ (
(
'--username',
borgmatic.hooks.credential.parse.resolve_credential(database['username']),
borgmatic.hooks.credential.parse.resolve_credential(database['username'], config),
)
if 'username' in database
else ()
@@ -97,9 +100,7 @@ def database_names_to_dump(database, extra_environment, dry_run):
+ (tuple(database['list_options'].split(' ')) if 'list_options' in database else ())
)
logger.debug('Querying for "all" PostgreSQL databases to dump')
list_output = execute_command_and_capture_output(
list_command, extra_environment=extra_environment
)
list_output = execute_command_and_capture_output(list_command, environment=environment)
return tuple(
row[0]
@@ -146,9 +147,9 @@ def dump_data_sources(
logger.info(f'Dumping PostgreSQL databases{dry_run_label}')
for database in databases:
extra_environment = make_extra_environment(database)
environment = make_environment(database, config)
dump_path = make_dump_path(borgmatic_runtime_directory)
dump_database_names = database_names_to_dump(database, extra_environment, dry_run)
dump_database_names = database_names_to_dump(database, config, environment, dry_run)
if not dump_database_names:
if dry_run:
@@ -189,7 +190,7 @@ def dump_data_sources(
'--username',
shlex.quote(
borgmatic.hooks.credential.parse.resolve_credential(
database['username']
database['username'], config
)
),
)
@@ -222,7 +223,7 @@ def dump_data_sources(
execute_command(
command,
shell=True,
extra_environment=extra_environment,
environment=environment,
)
else:
dump.create_named_pipe_for_dump(dump_filename)
@@ -230,7 +231,7 @@ def dump_data_sources(
execute_command(
command,
shell=True,
extra_environment=extra_environment,
environment=environment,
run_to_completion=False,
)
)
@@ -238,7 +239,8 @@ def dump_data_sources(
if not dry_run:
patterns.append(
borgmatic.borg.pattern.Pattern(
os.path.join(borgmatic_runtime_directory, 'postgresql_databases')
os.path.join(borgmatic_runtime_directory, 'postgresql_databases'),
source=borgmatic.borg.pattern.Pattern_source.HOOK,
)
)
@@ -309,8 +311,11 @@ def restore_data_source_dump(
connection_params['port'] or data_source.get('restore_port', data_source.get('port', ''))
)
username = borgmatic.hooks.credential.parse.resolve_credential(
connection_params['username']
or data_source.get('restore_username', data_source.get('username'))
(
connection_params['username']
or data_source.get('restore_username', data_source.get('username'))
),
config,
)
all_databases = bool(data_source['name'] == 'all')
@@ -363,9 +368,7 @@ def restore_data_source_dump(
)
)
extra_environment = make_extra_environment(
data_source, restore_connection_params=connection_params
)
environment = make_environment(data_source, config, restore_connection_params=connection_params)
logger.debug(f"Restoring PostgreSQL database {data_source['name']}{dry_run_label}")
if dry_run:
@@ -378,6 +381,6 @@ def restore_data_source_dump(
[extract_process] if extract_process else [],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout if extract_process else None,
extra_environment=extra_environment,
environment=environment,
)
execute_command(analyze_command, extra_environment=extra_environment)
execute_command(analyze_command, environment=environment)
+8 -6
View File
@@ -11,10 +11,10 @@ def get_contained_patterns(parent_directory, candidate_patterns):
paths, but there's a parent directory (logical volume, dataset, subvolume, etc.) at /var, then
/var is what we want to snapshot.
For this to work, a candidate pattern path can't have any globs or other non-literal characters
in the initial portion of the path that matches the parent directory. For instance, a parent
directory of /var would match a candidate pattern path of /var/log/*/data, but not a pattern
path like /v*/log/*/data.
For this function to work, a candidate pattern path can't have any globs or other non-literal
characters in the initial portion of the path that matches the parent directory. For instance, a
parent directory of /var would match a candidate pattern path of /var/log/*/data, but not a
pattern path like /v*/log/*/data.
The one exception is that if a regular expression pattern path starts with "^", that will get
stripped off for purposes of matching against a parent directory.
@@ -31,8 +31,10 @@ def get_contained_patterns(parent_directory, candidate_patterns):
candidate
for candidate in candidate_patterns
for candidate_path in (pathlib.PurePath(candidate.path.lstrip('^')),)
if pathlib.PurePath(parent_directory) == candidate_path
or pathlib.PurePath(parent_directory) in candidate_path.parents
if (
pathlib.PurePath(parent_directory) == candidate_path
or pathlib.PurePath(parent_directory) in candidate_path.parents
)
)
candidate_patterns -= set(contained_patterns)
+2 -1
View File
@@ -90,7 +90,8 @@ def dump_data_sources(
if not dry_run:
patterns.append(
borgmatic.borg.pattern.Pattern(
os.path.join(borgmatic_runtime_directory, 'sqlite_databases')
os.path.join(borgmatic_runtime_directory, 'sqlite_databases'),
source=borgmatic.borg.pattern.Pattern_source.HOOK,
)
)
+49 -11
View File
@@ -1,5 +1,6 @@
import collections
import glob
import hashlib
import logging
import os
import shutil
@@ -38,6 +39,9 @@ def get_datasets_to_backup(zfs_command, patterns):
pattern paths represent the requested datasets to snapshot. But also include any datasets tagged
with a borgmatic-specific user property, whether or not they appear in the patterns.
Only include datasets that contain at least one root pattern sourced from borgmatic
configuration (as opposed to generated elsewhere in borgmatic).
Return the result as a sequence of Dataset instances, sorted by mount point.
'''
list_output = borgmatic.execute.execute_command_and_capture_output(
@@ -48,7 +52,7 @@ def get_datasets_to_backup(zfs_command, patterns):
'-t',
'filesystem',
'-o',
f'name,mountpoint,{BORGMATIC_USER_PROPERTY}',
f'name,mountpoint,canmount,{BORGMATIC_USER_PROPERTY}',
)
)
@@ -60,7 +64,12 @@ def get_datasets_to_backup(zfs_command, patterns):
(
Dataset(dataset_name, mount_point, (user_property_value == 'auto'), ())
for line in list_output.splitlines()
for (dataset_name, mount_point, user_property_value) in (line.rstrip().split('\t'),)
for (dataset_name, mount_point, can_mount, user_property_value) in (
line.rstrip().split('\t'),
)
# Skip datasets that are marked "canmount=off", because mounting their snapshots will
# result in completely empty mount points—thereby preventing us from backing them up.
if can_mount == 'on'
),
key=lambda dataset: dataset.mount_point,
reverse=True,
@@ -83,7 +92,12 @@ def get_datasets_to_backup(zfs_command, patterns):
for contained_patterns in (
(
(
(borgmatic.borg.pattern.Pattern(dataset.mount_point),)
(
borgmatic.borg.pattern.Pattern(
dataset.mount_point,
source=borgmatic.borg.pattern.Pattern_source.HOOK,
),
)
if dataset.auto_backup
else ()
)
@@ -92,7 +106,12 @@ def get_datasets_to_backup(zfs_command, patterns):
)
),
)
if contained_patterns
if dataset.auto_backup
or any(
pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT
and pattern.source == borgmatic.borg.pattern.Pattern_source.CONFIG
for pattern in contained_patterns
)
),
key=lambda dataset: dataset.mount_point,
)
@@ -155,10 +174,14 @@ def mount_snapshot(mount_command, full_snapshot_name, snapshot_mount_path): # p
)
def make_borg_snapshot_pattern(pattern, normalized_runtime_directory):
MOUNT_POINT_HASH_LENGTH = 10
def make_borg_snapshot_pattern(pattern, dataset, normalized_runtime_directory):
'''
Given a Borg pattern as a borgmatic.borg.pattern.Pattern instance, return a new Pattern with its
path rewritten to be in a snapshot directory based on the given runtime directory.
Given a Borg pattern as a borgmatic.borg.pattern.Pattern instance and the Dataset containing it,
return a new Pattern with its path rewritten to be in a snapshot directory based on both the
given runtime directory and the given Dataset's mount point.
Move any initial caret in a regular expression pattern path to the beginning, so as not to break
the regular expression.
@@ -173,6 +196,10 @@ def make_borg_snapshot_pattern(pattern, normalized_runtime_directory):
rewritten_path = initial_caret + os.path.join(
normalized_runtime_directory,
'zfs_snapshots',
# Including this hash prevents conflicts between snapshot patterns for different datasets.
# For instance, without this, snapshotting a dataset at /var and another at /var/spool would
# result in overlapping snapshot patterns and therefore colliding mount attempts.
hashlib.shake_256(dataset.mount_point.encode('utf-8')).hexdigest(MOUNT_POINT_HASH_LENGTH),
'.', # Borg 1.4+ "slashdot" hack.
# Included so that the source directory ends up in the Borg archive at its "original" path.
pattern.path.lstrip('^').lstrip(os.path.sep),
@@ -183,6 +210,7 @@ def make_borg_snapshot_pattern(pattern, normalized_runtime_directory):
pattern.type,
pattern.style,
pattern.device,
source=borgmatic.borg.pattern.Pattern_source.HOOK,
)
@@ -209,7 +237,8 @@ def dump_data_sources(
dry_run_label = ' (dry run; not actually snapshotting anything)' if dry_run else ''
logger.info(f'Snapshotting ZFS datasets{dry_run_label}')
# List ZFS datasets to get their mount points.
# List ZFS datasets to get their mount points, but only consider those patterns that came from
# actual user configuration (as opposed to, say, other hooks).
zfs_command = hook_config.get('zfs_command', 'zfs')
requested_datasets = get_datasets_to_backup(zfs_command, patterns)
@@ -234,6 +263,9 @@ def dump_data_sources(
snapshot_mount_path = os.path.join(
normalized_runtime_directory,
'zfs_snapshots',
hashlib.shake_256(dataset.mount_point.encode('utf-8')).hexdigest(
MOUNT_POINT_HASH_LENGTH
),
dataset.mount_point.lstrip(os.path.sep),
)
@@ -249,7 +281,9 @@ def dump_data_sources(
)
for pattern in dataset.contained_patterns:
snapshot_pattern = make_borg_snapshot_pattern(pattern, normalized_runtime_directory)
snapshot_pattern = make_borg_snapshot_pattern(
pattern, dataset, normalized_runtime_directory
)
# Attempt to update the pattern in place, since pattern order matters to Borg.
try:
@@ -334,6 +368,7 @@ def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, d
os.path.normpath(borgmatic_runtime_directory),
),
'zfs_snapshots',
'*',
)
logger.debug(f'Looking for snapshots to remove in {snapshots_glob}{dry_run_label}')
umount_command = hook_config.get('umount_command', 'umount')
@@ -346,7 +381,10 @@ def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, d
# child datasets before the shorter mount point paths of parent datasets.
for mount_point in reversed(dataset_mount_points):
snapshot_mount_path = os.path.join(snapshots_directory, mount_point.lstrip(os.path.sep))
if not os.path.isdir(snapshot_mount_path):
# If the snapshot mount path is empty, this is probably just a "shadow" of a nested
# dataset and therefore there's nothing to unmount.
if not os.path.isdir(snapshot_mount_path) or not os.listdir(snapshot_mount_path):
continue
# This might fail if the path is already mounted, but we swallow errors here since we'll
@@ -370,7 +408,7 @@ def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, d
return
except subprocess.CalledProcessError as error:
logger.debug(error)
return
continue
if not dry_run:
shutil.rmtree(snapshots_directory)
+3 -3
View File
@@ -51,13 +51,13 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
try:
username = borgmatic.hooks.credential.parse.resolve_credential(
hook_config.get('username')
hook_config.get('username'), config
)
password = borgmatic.hooks.credential.parse.resolve_credential(
hook_config.get('password')
hook_config.get('password'), config
)
access_token = borgmatic.hooks.credential.parse.resolve_credential(
hook_config.get('access_token')
hook_config.get('access_token'), config
)
except ValueError as error:
logger.warning(f'Ntfy credential error: {error}')
+1 -1
View File
@@ -42,7 +42,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
try:
integration_key = borgmatic.hooks.credential.parse.resolve_credential(
hook_config.get('integration_key')
hook_config.get('integration_key'), config
)
except ValueError as error:
logger.warning(f'PagerDuty credential error: {error}')
+4 -2
View File
@@ -35,8 +35,10 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
state_config = hook_config.get(state.name.lower(), {})
try:
token = borgmatic.hooks.credential.parse.resolve_credential(hook_config.get('token'))
user = borgmatic.hooks.credential.parse.resolve_credential(hook_config.get('user'))
token = borgmatic.hooks.credential.parse.resolve_credential(
hook_config.get('token'), config
)
user = borgmatic.hooks.credential.parse.resolve_credential(hook_config.get('user'), config)
except ValueError as error:
logger.warning(f'Pushover credential error: {error}')
return
+9 -3
View File
@@ -37,9 +37,15 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
)
try:
username = borgmatic.hooks.credential.parse.resolve_credential(hook_config.get('username'))
password = borgmatic.hooks.credential.parse.resolve_credential(hook_config.get('password'))
api_key = borgmatic.hooks.credential.parse.resolve_credential(hook_config.get('api_key'))
username = borgmatic.hooks.credential.parse.resolve_credential(
hook_config.get('username'), config
)
password = borgmatic.hooks.credential.parse.resolve_credential(
hook_config.get('password'), config
)
api_key = borgmatic.hooks.credential.parse.resolve_credential(
hook_config.get('api_key'), config
)
except ValueError as error:
logger.warning(f'Zabbix credential error: {error}')
return
+2 -1
View File
@@ -710,7 +710,8 @@ zabbix:
- fail
```
This hook requires the Zabbix server be running version 7.0+
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.)
### Authentication methods
+165 -19
View File
@@ -19,6 +19,7 @@ encryption_passphrase: yourpassphrase
But if you'd rather store them outside of borgmatic, whether for convenience
or security reasons, read on.
### Delegating to another application
borgmatic supports calling another application such as a password manager to
@@ -31,26 +32,17 @@ to provide the passphrase:
encryption_passcommand: pass path/to/borg-passphrase
```
Another example for [KeePassXC](https://keepassxc.org/):
```yaml
encryption_passcommand: keepassxc-cli show --show-protected --attributes Password credentials.kdbx borg_passphrase
```
... where `borg_passphrase` is the title of the KeePassXC entry containing your
Borg encryption passphrase in its `Password` field.
<span class="minilink minilink-addedin">New in version 1.9.9</span> Instead of
letting Borg run the passcommand—potentially multiple times since borgmatic runs
Borg multiple times—borgmatic now runs the passcommand itself and passes the
resulting passprhase securely to Borg via an anonymous pipe. This means you
resulting passphrase securely to Borg via an anonymous pipe. This means you
should only ever get prompted for your password manager's passphrase at most
once per borgmatic run.
### Using systemd service credentials
### systemd service credentials
borgmatic supports using encrypted [systemd
borgmatic supports reading encrypted [systemd
credentials](https://systemd.io/CREDENTIALS/). To use this feature, start by
saving your password as an encrypted credential to
`/etc/credstore.encrypted/borgmatic.pw`, e.g.,
@@ -146,13 +138,172 @@ The one exception is `borgmatic config validate`, which doesn't actually load
any credentials and should continue working anywhere.
### Container secrets
<span class="minilink minilink-addedin">New in version 1.9.11</span> When
running inside a container, borgmatic can read [Docker
secrets](https://docs.docker.com/compose/how-tos/use-secrets/) and [Podman
secrets](https://www.redhat.com/en/blog/new-podman-secrets-command). Creating
those secrets and passing them into your borgmatic container is outside the
scope of this documentation, but here's a simple example of that with [Docker
Compose](https://docs.docker.com/compose/):
```yaml
services:
borgmatic:
# Use the actual image name of your borgmatic container here.
image: borgmatic:latest
secrets:
- borgmatic_passphrase
secrets:
borgmatic_passphrase:
file: /etc/borgmatic/passphrase.txt
```
This assumes there's a file on the host at `/etc/borgmatic/passphrase.txt`
containing your passphrase. Docker or Podman mounts the contents of that file
into a secret named `borgmatic_passphrase` in the borgmatic container at
`/run/secrets/`.
Once your container secret is in place, you can consume it within your borgmatic
configuration file:
```yaml
encryption_passphrase: "{credential container borgmatic_passphrase}"
```
This reads the secret securely from a file mounted at
`/run/secrets/borgmatic_passphrase` within the borgmatic container.
The `{credential ...}` syntax works for several different options in a borgmatic
configuration file besides just `encryption_passphrase`. For instance, the
username, password, and API token options within database and monitoring hooks
support `{credential ...}`:
```yaml
postgresql_databases:
- name: invoices
username: postgres
password: "{credential container borgmatic_db1}"
```
For specifics about which options are supported, see the
[configuration
reference](https://torsion.org/borgmatic/docs/reference/configuration/).
You can also optionally override the `/run/secrets` directory that borgmatic reads secrets from
inside a container:
```yaml
container:
secrets_directory: /path/to/secrets
```
But you should only need to do this for development or testing purposes.
### KeePassXC passwords
<span class="minilink minilink-addedin">New in version 1.9.11</span> borgmatic
supports reading passwords from the [KeePassXC](https://keepassxc.org/) password
manager. To use this feature, start by creating an entry in your KeePassXC
database, putting your password into the "Password" field of that entry and
making sure it's saved.
Then, you can consume that password in your borgmatic configuration file. For
instance, if the entry's title is "borgmatic" and your KeePassXC database is
located at `/etc/keys.kdbx`, do this:
```yaml
encryption_passphrase: "{credential keepassxc /etc/keys.kdbx borgmatic}"
```
But if the entry's title is multiple words like `borg pw`, you'll
need to quote it:
```yaml
encryption_passphrase: "{credential keepassxc /etc/keys.kdbx 'borg pw'}"
```
With this in place, borgmatic runs the `keepassxc-cli` command to retrieve the
passphrase on demand. But note that `keepassxc-cli` will prompt for its own
passphrase in order to unlock its database, so be prepared to enter it when
running borgmatic.
The `{credential ...}` syntax works for several different options in a borgmatic
configuration file besides just `encryption_passphrase`. For instance, the
username, password, and API token options within database and monitoring hooks
support `{credential ...}`:
```yaml
postgresql_databases:
- name: invoices
username: postgres
password: "{credential keepassxc /etc/keys.kdbx database}"
```
For specifics about which options are supported, see the
[configuration
reference](https://torsion.org/borgmatic/docs/reference/configuration/).
You can also optionally override the `keepassxc-cli` command that borgmatic calls to load
passwords:
```yaml
keepassxc:
keepassxc_cli_command: /usr/local/bin/keepassxc-cli
```
### File-based credentials
<span class="minilink minilink-addedin">New in version 1.9.11</span> borgmatic
supports reading credentials from arbitrary file paths. To use this feature,
start by writing your credential into a file that borgmatic has permission to
read. Take care not to include anything in the file other than your credential.
(borgmatic is smart enough to strip off a trailing newline though.)
You can consume that credential file in your borgmatic configuration. For
instance, if your credential file is at `/credentials/borgmatic.txt`, do this:
```yaml
encryption_passphrase: "{credential file /credentials/borgmatic.txt}"
```
With this in place, borgmatic reads the credential from the file path.
The `{credential ...}` syntax works for several different options in a borgmatic
configuration file besides just `encryption_passphrase`. For instance, the
username, password, and API token options within database and monitoring hooks
support `{credential ...}`:
```yaml
postgresql_databases:
- name: invoices
username: postgres
password: "{credential file /credentials/database.txt}"
```
For specifics about which options are supported, see the
[configuration
reference](https://torsion.org/borgmatic/docs/reference/configuration/).
### Environment variable interpolation
<span class="minilink minilink-addedin">New in version 1.6.4</span> borgmatic
supports interpolating arbitrary environment variables directly into option
values in your configuration file. That means you can instruct borgmatic to
pull your repository passphrase, your database passwords, or any other option
values from environment variables. For instance:
values from environment variables.
Be aware though that environment variables may be less secure than some of the
other approaches above for getting credentials into borgmatic. That's because
environment variables may be visible from within child processes and/or OS-level
process metadata.
Here's an example of using an environment variable from borgmatic's
configuration file:
```yaml
encryption_passphrase: ${YOUR_PASSPHRASE}
@@ -214,6 +365,7 @@ can escape it with a backslash. For instance, if your password is literally
encryption_passphrase: \${A}@!
```
## Related features
Another way to override particular options within a borgmatic configuration
@@ -226,9 +378,3 @@ Additionally, borgmatic action hooks support their own [variable
interpolation](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/#variable-interpolation),
although in that case it's for particular borgmatic runtime values rather than
(only) environment variables.
Lastly, if you do want to specify your passhprase directly within borgmatic
configuration, but you'd like to keep it in a separate file from your main
configuration, you can [use a configuration include or a merge
include](https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/#configuration-includes)
to pull in an external password.
+11 -4
View File
@@ -54,8 +54,8 @@ You have a couple of options for borgmatic to find and backup your ZFS datasets:
* For any dataset you'd like backed up, add its mount point to borgmatic's
`source_directories` option.
* <span class="minilink minilink-addedin">New in version 1.9.6</span> Or
include the mount point with borgmatic's `patterns` or `patterns_from`
options.
include the mount point as a root pattern with borgmatic's `patterns` or
`patterns_from` options.
* Or set the borgmatic-specific user property
`org.torsion.borgmatic:backup=auto` onto your dataset, e.g. by running `zfs
set org.torsion.borgmatic:backup=auto datasetname`. Then borgmatic can find
@@ -65,6 +65,11 @@ If you have multiple borgmatic configuration files with ZFS enabled, and you'd
like particular datasets to be backed up only for particular configuration
files, use the `source_directories` option instead of the user property.
<span class="minilink minilink-addedin">New in version 1.9.11</span> borgmatic
won't snapshot datasets with the `canmount=off` property, which is often set on
datasets that only serve as a container for other datasets. Use `zfs get
canmount datasetname` to see the `canmount` value for a dataset.
During a backup, borgmatic automatically snapshots these discovered datasets
(non-recursively), temporarily mounts the snapshots within its [runtime
directory](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#runtime-directory),
@@ -147,7 +152,8 @@ For any subvolume you'd like backed up, add its path to borgmatic's
`source_directories` option.
<span class="minilink minilink-addedin">New in version 1.9.6</span> Or include
the mount point with borgmatic's `patterns` or `patterns_from` options.
the mount point as a root pattern with borgmatic's `patterns` or `patterns_from`
options.
During a backup, borgmatic snapshots these subvolumes (non-recursively) and
includes the snapshotted files in the paths sent to Borg. borgmatic is also
@@ -252,7 +258,8 @@ For any logical volume you'd like backed up, add its mount point to
borgmatic's `source_directories` option.
<span class="minilink minilink-addedin">New in version 1.9.6</span> Or include
the mount point with borgmatic's `patterns` or `patterns_from` options.
the mount point as a root pattern with borgmatic's `patterns` or `patterns_from`
options.
During a backup, borgmatic automatically snapshots these discovered logical volumes
(non-recursively), temporarily mounts the snapshots within its [runtime
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "borgmatic"
version = "1.9.10"
version = "1.9.11"
authors = [
{ name="Dan Helfman", email="witten@torsion.org" },
]
@@ -0,0 +1,29 @@
import argparse
import sys
def parse_arguments(*unparsed_arguments):
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('command')
parser.add_argument('--show-protected', action='store_true')
parser.add_argument('--attributes')
parser.add_argument('database_path')
parser.add_argument('attribute_name')
return parser.parse_args(unparsed_arguments)
def main():
arguments = parse_arguments(*sys.argv[1:])
assert arguments.command == 'show'
assert arguments.show_protected
assert arguments.attributes == 'Password'
assert arguments.database_path.endswith('.kdbx')
assert arguments.attribute_name
print('test')
if __name__ == '__main__':
main()
+2
View File
@@ -27,6 +27,7 @@ BUILTIN_DATASETS = (
'used': '256K',
'avail': '23.7M',
'refer': '25K',
'canmount': 'on',
'mountpoint': '/pool',
},
{
@@ -34,6 +35,7 @@ BUILTIN_DATASETS = (
'used': '256K',
'avail': '23.7M',
'refer': '25K',
'canmount': 'on',
'mountpoint': '/pool/dataset',
},
)
@@ -0,0 +1,68 @@
import json
import os
import shutil
import subprocess
import sys
import tempfile
def generate_configuration(config_path, repository_path, secrets_directory):
'''
Generate borgmatic configuration into a file at the config path, and update the defaults so as
to work for testing, including updating the source directories, injecting the given repository
path, and tacking on an encryption passphrase loaded from container secrets in the given secrets
directory.
'''
subprocess.check_call(f'borgmatic config generate --destination {config_path}'.split(' '))
config = (
open(config_path)
.read()
.replace('ssh://user@backupserver/./sourcehostname.borg', repository_path)
.replace('- path: /mnt/backup', '')
.replace('label: local', '')
.replace('- /home/user/path with spaces', '')
.replace('- /home', f'- {config_path}')
.replace('- /etc', '')
.replace('- /var/log/syslog*', '')
+ '\nencryption_passphrase: "{credential container mysecret}"'
+ f'\ncontainer:\n secrets_directory: {secrets_directory}'
)
config_file = open(config_path, 'w')
config_file.write(config)
config_file.close()
def test_container_secret():
# Create a Borg repository.
temporary_directory = tempfile.mkdtemp()
repository_path = os.path.join(temporary_directory, 'test.borg')
original_working_directory = os.getcwd()
os.chdir(temporary_directory)
try:
config_path = os.path.join(temporary_directory, 'test.yaml')
generate_configuration(config_path, repository_path, secrets_directory=temporary_directory)
secret_path = os.path.join(temporary_directory, 'mysecret')
with open(secret_path, 'w') as secret_file:
secret_file.write('test')
subprocess.check_call(
f'borgmatic -v 2 --config {config_path} repo-create --encryption repokey'.split(' '),
)
# Run borgmatic to generate a backup archive, and then list it to make sure it exists.
subprocess.check_call(
f'borgmatic --config {config_path}'.split(' '),
)
output = subprocess.check_output(
f'borgmatic --config {config_path} list --json'.split(' '),
).decode(sys.stdout.encoding)
parsed_output = json.loads(output)
assert len(parsed_output) == 1
assert len(parsed_output[0]['archives']) == 1
finally:
os.chdir(original_working_directory)
shutil.rmtree(temporary_directory)
@@ -0,0 +1,68 @@
import json
import os
import shutil
import subprocess
import sys
import tempfile
def generate_configuration(config_path, repository_path, credential_path):
'''
Generate borgmatic configuration into a file at the config path, and update the defaults so as
to work for testing, including updating the source directories, injecting the given repository
path, and tacking on an encryption passphrase loaded from file at the given credential path.
'''
subprocess.check_call(f'borgmatic config generate --destination {config_path}'.split(' '))
config = (
open(config_path)
.read()
.replace('ssh://user@backupserver/./sourcehostname.borg', repository_path)
.replace('- path: /mnt/backup', '')
.replace('label: local', '')
.replace('- /home/user/path with spaces', '')
.replace('- /home', f'- {config_path}')
.replace('- /etc', '')
.replace('- /var/log/syslog*', '')
+ '\nencryption_passphrase: "{credential file '
+ credential_path
+ '}"'
)
config_file = open(config_path, 'w')
config_file.write(config)
config_file.close()
def test_file_credential():
# Create a Borg repository.
temporary_directory = tempfile.mkdtemp()
repository_path = os.path.join(temporary_directory, 'test.borg')
original_working_directory = os.getcwd()
os.chdir(temporary_directory)
try:
config_path = os.path.join(temporary_directory, 'test.yaml')
credential_path = os.path.join(temporary_directory, 'mycredential')
generate_configuration(config_path, repository_path, credential_path)
with open(credential_path, 'w') as credential_file:
credential_file.write('test')
subprocess.check_call(
f'borgmatic -v 2 --config {config_path} repo-create --encryption repokey'.split(' '),
)
# Run borgmatic to generate a backup archive, and then list it to make sure it exists.
subprocess.check_call(
f'borgmatic --config {config_path}'.split(' '),
)
output = subprocess.check_output(
f'borgmatic --config {config_path} list --json'.split(' '),
).decode(sys.stdout.encoding)
parsed_output = json.loads(output)
assert len(parsed_output) == 1
assert len(parsed_output[0]['archives']) == 1
finally:
os.chdir(original_working_directory)
shutil.rmtree(temporary_directory)
@@ -0,0 +1,67 @@
import json
import os
import shutil
import subprocess
import sys
import tempfile
def generate_configuration(config_path, repository_path):
'''
Generate borgmatic configuration into a file at the config path, and update the defaults so as
to work for testing, including updating the source directories, injecting the given repository
path, and tacking on an encryption passphrase loaded from keepassxc-cli.
'''
subprocess.check_call(f'borgmatic config generate --destination {config_path}'.split(' '))
config = (
open(config_path)
.read()
.replace('ssh://user@backupserver/./sourcehostname.borg', repository_path)
.replace('- path: /mnt/backup', '')
.replace('label: local', '')
.replace('- /home/user/path with spaces', '')
.replace('- /home', f'- {config_path}')
.replace('- /etc', '')
.replace('- /var/log/syslog*', '')
+ '\nencryption_passphrase: "{credential keepassxc keys.kdbx mypassword}"'
+ '\nkeepassxc:\n keepassxc_cli_command: python3 /app/tests/end-to-end/commands/fake_keepassxc_cli.py'
)
config_file = open(config_path, 'w')
config_file.write(config)
config_file.close()
def test_keepassxc_password():
# Create a Borg repository.
temporary_directory = tempfile.mkdtemp()
repository_path = os.path.join(temporary_directory, 'test.borg')
original_working_directory = os.getcwd()
os.chdir(temporary_directory)
try:
config_path = os.path.join(temporary_directory, 'test.yaml')
generate_configuration(config_path, repository_path)
database_path = os.path.join(temporary_directory, 'keys.kdbx')
with open(database_path, 'w') as database_file:
database_file.write('fake KeePassXC database to pacify file existence check')
subprocess.check_call(
f'borgmatic -v 2 --config {config_path} repo-create --encryption repokey'.split(' '),
)
# Run borgmatic to generate a backup archive, and then list it to make sure it exists.
subprocess.check_call(
f'borgmatic --config {config_path}'.split(' '),
)
output = subprocess.check_output(
f'borgmatic --config {config_path} list --json'.split(' '),
).decode(sys.stdout.encoding)
parsed_output = json.loads(output)
assert len(parsed_output) == 1
assert len(parsed_output[0]['archives']) == 1
finally:
os.chdir(original_working_directory)
shutil.rmtree(temporary_directory)
@@ -30,15 +30,13 @@ def generate_configuration(config_path, repository_path):
config_file.close()
def test_borgmatic_command():
def test_systemd_credential():
# Create a Borg repository.
temporary_directory = tempfile.mkdtemp()
repository_path = os.path.join(temporary_directory, 'test.borg')
extract_path = os.path.join(temporary_directory, 'extract')
original_working_directory = os.getcwd()
os.mkdir(extract_path)
os.chdir(extract_path)
os.chdir(temporary_directory)
try:
config_path = os.path.join(temporary_directory, 'test.yaml')
+21 -10
View File
@@ -5,16 +5,21 @@ import pytest
from flexmock import flexmock
from borgmatic.actions import create as module
from borgmatic.borg.pattern import Pattern, Pattern_style, Pattern_type
from borgmatic.borg.pattern import Pattern, Pattern_source, Pattern_style, Pattern_type
@pytest.mark.parametrize(
'pattern_line,expected_pattern',
(
('R /foo', Pattern('/foo')),
('P sh', Pattern('sh', Pattern_type.PATTERN_STYLE)),
('+ /foo*', Pattern('/foo*', Pattern_type.INCLUDE)),
('+ sh:/foo*', Pattern('/foo*', Pattern_type.INCLUDE, Pattern_style.SHELL)),
('R /foo', Pattern('/foo', source=Pattern_source.CONFIG)),
('P sh', Pattern('sh', Pattern_type.PATTERN_STYLE, source=Pattern_source.CONFIG)),
('+ /foo*', Pattern('/foo*', Pattern_type.INCLUDE, source=Pattern_source.CONFIG)),
(
'+ sh:/foo*',
Pattern(
'/foo*', Pattern_type.INCLUDE, Pattern_style.SHELL, source=Pattern_source.CONFIG
),
),
),
)
def test_parse_pattern_transforms_pattern_line_to_instance(pattern_line, expected_pattern):
@@ -28,8 +33,8 @@ def test_parse_pattern_with_invalid_pattern_line_errors():
def test_collect_patterns_converts_source_directories():
assert module.collect_patterns({'source_directories': ['/foo', '/bar']}) == (
Pattern('/foo'),
Pattern('/bar'),
Pattern('/foo', source=Pattern_source.CONFIG),
Pattern('/bar', source=Pattern_source.CONFIG),
)
@@ -48,9 +53,15 @@ def test_collect_patterns_parses_config_patterns():
def test_collect_patterns_converts_exclude_patterns():
assert module.collect_patterns({'exclude_patterns': ['/foo', '/bar', 'sh:**/baz']}) == (
Pattern('/foo', Pattern_type.NO_RECURSE, Pattern_style.FNMATCH),
Pattern('/bar', Pattern_type.NO_RECURSE, Pattern_style.FNMATCH),
Pattern('**/baz', Pattern_type.NO_RECURSE, Pattern_style.SHELL),
Pattern(
'/foo', Pattern_type.NO_RECURSE, Pattern_style.FNMATCH, source=Pattern_source.CONFIG
),
Pattern(
'/bar', Pattern_type.NO_RECURSE, Pattern_style.FNMATCH, source=Pattern_source.CONFIG
),
Pattern(
'**/baz', Pattern_type.NO_RECURSE, Pattern_style.SHELL, source=Pattern_source.CONFIG
),
)
+15 -15
View File
@@ -17,7 +17,7 @@ def test_run_arbitrary_borg_calls_borg_with_flags():
('borg', 'break-lock', '::'),
output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
shell=True,
extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -41,7 +41,7 @@ def test_run_arbitrary_borg_with_log_info_calls_borg_with_info_flag():
('borg', 'break-lock', '--info', '::'),
output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
shell=True,
extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -66,7 +66,7 @@ def test_run_arbitrary_borg_with_log_debug_calls_borg_with_debug_flag():
('borg', 'break-lock', '--debug', '--show-rc', '::'),
output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
shell=True,
extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -94,7 +94,7 @@ def test_run_arbitrary_borg_with_lock_wait_calls_borg_with_lock_wait_flags():
('borg', 'break-lock', '--lock-wait', '5', '::'),
output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
shell=True,
extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -118,7 +118,7 @@ def test_run_arbitrary_borg_with_archive_calls_borg_with_archive_flag():
('borg', 'break-lock', "'::$ARCHIVE'"),
output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
shell=True,
extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': 'archive'},
environment={'BORG_REPO': 'repo', 'ARCHIVE': 'archive'},
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -143,7 +143,7 @@ def test_run_arbitrary_borg_with_local_path_calls_borg_via_local_path():
('borg1', 'break-lock', '::'),
output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
shell=True,
extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
working_directory=None,
borg_local_path='borg1',
borg_exit_codes=None,
@@ -169,7 +169,7 @@ def test_run_arbitrary_borg_with_exit_codes_calls_borg_using_them():
('borg', 'break-lock', '::'),
output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
shell=True,
extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
working_directory=None,
borg_local_path='borg',
borg_exit_codes=borg_exit_codes,
@@ -195,7 +195,7 @@ def test_run_arbitrary_borg_with_remote_path_calls_borg_with_remote_path_flags()
('borg', 'break-lock', '--remote-path', 'borg1', '::'),
output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
shell=True,
extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -222,7 +222,7 @@ def test_run_arbitrary_borg_with_remote_path_injection_attack_gets_escaped():
('borg', 'break-lock', '--remote-path', "'borg1; naughty-command'", '::'),
output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
shell=True,
extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -247,7 +247,7 @@ def test_run_arbitrary_borg_passes_borg_specific_flags_to_borg():
('borg', 'list', '--progress', '::'),
output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
shell=True,
extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -271,7 +271,7 @@ def test_run_arbitrary_borg_omits_dash_dash_in_flags_passed_to_borg():
('borg', 'break-lock', '::'),
output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
shell=True,
extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -295,7 +295,7 @@ def test_run_arbitrary_borg_without_borg_specific_flags_does_not_raise():
('borg',),
output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
shell=True,
extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -319,7 +319,7 @@ def test_run_arbitrary_borg_passes_key_sub_command_to_borg_before_injected_flags
('borg', 'key', 'export', '--info', '::'),
output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
shell=True,
extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -344,7 +344,7 @@ def test_run_arbitrary_borg_passes_debug_sub_command_to_borg_before_injected_fla
('borg', 'debug', 'dump-manifest', '--info', '::', 'path'),
output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
shell=True,
extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -371,7 +371,7 @@ def test_run_arbitrary_borg_calls_borg_with_working_directory():
('borg', 'break-lock', '::'),
output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
shell=True,
extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
working_directory='/working/dir',
borg_local_path='borg',
borg_exit_codes=None,
+1 -1
View File
@@ -14,7 +14,7 @@ def insert_execute_command_mock(command, working_directory=None, borg_exit_codes
)
flexmock(module).should_receive('execute_command').with_args(
command,
extra_environment=None,
environment=None,
working_directory=working_directory,
borg_local_path=command[0],
borg_exit_codes=borg_exit_codes,
+1 -1
View File
@@ -25,7 +25,7 @@ def insert_execute_command_mock(
command,
output_file=output_file,
output_log_level=module.logging.ANSWER,
extra_environment=None,
environment=None,
working_directory=working_directory,
borg_local_path=command[0],
borg_exit_codes=borg_exit_codes,
+7 -7
View File
@@ -18,7 +18,7 @@ def insert_execute_command_mock(
flexmock(module).should_receive('execute_command').with_args(
command,
output_file=output_file,
extra_environment=None,
environment=None,
working_directory=working_directory,
borg_local_path=command[0],
borg_exit_codes=borg_exit_codes,
@@ -342,7 +342,7 @@ def test_check_archives_with_progress_passes_through_to_borg():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'check', '--progress', 'repo'),
output_file=module.DO_NOT_CAPTURE,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -377,7 +377,7 @@ def test_check_archives_with_repair_passes_through_to_borg():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'check', '--repair', 'repo'),
output_file=module.DO_NOT_CAPTURE,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -412,7 +412,7 @@ def test_check_archives_with_max_duration_flag_passes_through_to_borg():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'check', '--max-duration', '33', 'repo'),
output_file=None,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -447,7 +447,7 @@ def test_check_archives_with_max_duration_option_passes_through_to_borg():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'check', '--max-duration', '33', 'repo'),
output_file=None,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -610,7 +610,7 @@ def test_check_archives_with_max_duration_flag_overrides_max_duration_option():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'check', '--max-duration', '44', 'repo'),
output_file=None,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -963,7 +963,7 @@ def test_check_archives_with_match_archives_passes_through_to_borg():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'check', '--match-archives', 'foo-*', 'repo'),
output_file=None,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
+1 -1
View File
@@ -17,7 +17,7 @@ def insert_execute_command_mock(
flexmock(module).should_receive('execute_command').with_args(
compact_command,
output_log_level=output_log_level,
extra_environment=None,
environment=None,
working_directory=working_directory,
borg_local_path=compact_command[0],
borg_exit_codes=borg_exit_codes,
+51 -43
View File
@@ -4,7 +4,7 @@ import pytest
from flexmock import flexmock
from borgmatic.borg import create as module
from borgmatic.borg.pattern import Pattern, Pattern_style, Pattern_type
from borgmatic.borg.pattern import Pattern, Pattern_source, Pattern_style, Pattern_type
from ..test_verbosity import insert_logging_mock
@@ -185,6 +185,12 @@ def test_any_parent_directories_treats_unrelated_paths_as_non_match():
def test_collect_special_file_paths_parses_special_files_from_borg_dry_run_file_list():
flexmock(module.flags).should_receive('omit_flag').replace_with(
lambda arguments, flag: arguments
)
flexmock(module.flags).should_receive('omit_flag_and_value').replace_with(
lambda arguments, flag: arguments
)
flexmock(module.environment).should_receive('make_environment').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
'Processing files ...\n- /foo\n+ /bar\n- /baz'
@@ -204,6 +210,12 @@ def test_collect_special_file_paths_parses_special_files_from_borg_dry_run_file_
def test_collect_special_file_paths_skips_borgmatic_runtime_directory():
flexmock(module.flags).should_receive('omit_flag').replace_with(
lambda arguments, flag: arguments
)
flexmock(module.flags).should_receive('omit_flag_and_value').replace_with(
lambda arguments, flag: arguments
)
flexmock(module.environment).should_receive('make_environment').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
'+ /foo\n- /run/borgmatic/bar\n- /baz'
@@ -231,6 +243,12 @@ def test_collect_special_file_paths_skips_borgmatic_runtime_directory():
def test_collect_special_file_paths_with_borgmatic_runtime_directory_missing_from_paths_output_errors():
flexmock(module.flags).should_receive('omit_flag').replace_with(
lambda arguments, flag: arguments
)
flexmock(module.flags).should_receive('omit_flag_and_value').replace_with(
lambda arguments, flag: arguments
)
flexmock(module.environment).should_receive('make_environment').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
'+ /foo\n- /bar\n- /baz'
@@ -251,6 +269,12 @@ def test_collect_special_file_paths_with_borgmatic_runtime_directory_missing_fro
def test_collect_special_file_paths_with_dry_run_and_borgmatic_runtime_directory_missing_from_paths_output_does_not_raise():
flexmock(module.flags).should_receive('omit_flag').replace_with(
lambda arguments, flag: arguments
)
flexmock(module.flags).should_receive('omit_flag_and_value').replace_with(
lambda arguments, flag: arguments
)
flexmock(module.environment).should_receive('make_environment').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
'+ /foo\n- /bar\n- /baz'
@@ -270,6 +294,12 @@ def test_collect_special_file_paths_with_dry_run_and_borgmatic_runtime_directory
def test_collect_special_file_paths_excludes_non_special_files():
flexmock(module.flags).should_receive('omit_flag').replace_with(
lambda arguments, flag: arguments
)
flexmock(module.flags).should_receive('omit_flag_and_value').replace_with(
lambda arguments, flag: arguments
)
flexmock(module.environment).should_receive('make_environment').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
'+ /foo\n+ /bar\n+ /baz'
@@ -290,30 +320,6 @@ def test_collect_special_file_paths_excludes_non_special_files():
) == ('/foo', '/baz')
def test_collect_special_file_paths_omits_exclude_no_dump_flag_from_command():
flexmock(module.environment).should_receive('make_environment').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'create', '--dry-run', '--list'),
capture_stderr=True,
working_directory=None,
extra_environment=None,
borg_local_path='borg',
borg_exit_codes=None,
).and_return('Processing files ...\n- /foo\n+ /bar\n- /baz').once()
flexmock(module).should_receive('special_file').and_return(True)
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module).should_receive('any_parent_directories').never()
module.collect_special_file_paths(
dry_run=False,
create_command=('borg', 'create', '--exclude-nodump'),
config={},
local_path='borg',
working_directory=None,
borgmatic_runtime_directory='/run/borgmatic',
)
DEFAULT_ARCHIVE_NAME = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}' # noqa: FS003
REPO_ARCHIVE = (f'repo::{DEFAULT_ARCHIVE_NAME}',)
@@ -663,6 +669,7 @@ def test_make_base_create_command_with_stream_processes_ignores_read_special_fal
'/dev/null',
Pattern_type.NO_RECURSE,
Pattern_style.FNMATCH,
source=Pattern_source.INTERNAL,
),
),
'/run/borgmatic',
@@ -713,6 +720,7 @@ def test_make_base_create_command_without_patterns_and_with_stream_processes_ign
'/dev/null',
Pattern_type.NO_RECURSE,
Pattern_style.FNMATCH,
source=Pattern_source.INTERNAL,
),
),
'/run/borgmatic',
@@ -969,7 +977,7 @@ def test_create_archive_calls_borg_with_parameters():
borg_local_path='borg',
borg_exit_codes=None,
working_directory=None,
extra_environment=None,
environment=None,
)
module.create_archive(
@@ -1003,7 +1011,7 @@ def test_create_archive_calls_borg_with_environment():
borg_local_path='borg',
borg_exit_codes=None,
working_directory=None,
extra_environment=environment,
environment=environment,
)
module.create_archive(
@@ -1036,7 +1044,7 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter():
borg_local_path='borg',
borg_exit_codes=None,
working_directory=None,
extra_environment=None,
environment=None,
)
insert_logging_mock(logging.INFO)
@@ -1066,7 +1074,7 @@ def test_create_archive_with_log_info_and_json_suppresses_most_borg_output():
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'create', '--json') + REPO_ARCHIVE,
working_directory=None,
extra_environment=None,
environment=None,
borg_local_path='borg',
borg_exit_codes=None,
)
@@ -1103,7 +1111,7 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
borg_local_path='borg',
borg_exit_codes=None,
working_directory=None,
extra_environment=None,
environment=None,
)
insert_logging_mock(logging.DEBUG)
@@ -1133,7 +1141,7 @@ def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output():
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'create', '--json') + REPO_ARCHIVE,
working_directory=None,
extra_environment=None,
environment=None,
borg_local_path='borg',
borg_exit_codes=None,
)
@@ -1172,7 +1180,7 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats():
borg_local_path='borg',
borg_exit_codes=None,
working_directory=None,
extra_environment=None,
environment=None,
)
insert_logging_mock(logging.INFO)
@@ -1209,7 +1217,7 @@ def test_create_archive_with_working_directory_calls_borg_with_working_directory
borg_local_path='borg',
borg_exit_codes=None,
working_directory='/working/dir',
extra_environment=None,
environment=None,
)
module.create_archive(
@@ -1244,7 +1252,7 @@ def test_create_archive_with_exit_codes_calls_borg_using_them():
borg_local_path='borg',
borg_exit_codes=borg_exit_codes,
working_directory=None,
extra_environment=None,
environment=None,
)
module.create_archive(
@@ -1278,7 +1286,7 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_answer_ou
borg_local_path='borg',
borg_exit_codes=None,
working_directory=None,
extra_environment=None,
environment=None,
)
module.create_archive(
@@ -1316,7 +1324,7 @@ def test_create_archive_with_files_calls_borg_with_answer_output_log_level():
borg_local_path='borg',
borg_exit_codes=None,
working_directory=None,
extra_environment=None,
environment=None,
)
module.create_archive(
@@ -1350,7 +1358,7 @@ def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_para
borg_local_path='borg',
borg_exit_codes=None,
working_directory=None,
extra_environment=None,
environment=None,
)
insert_logging_mock(logging.INFO)
@@ -1385,7 +1393,7 @@ def test_create_archive_with_progress_calls_borg_with_progress_parameter():
borg_local_path='borg',
borg_exit_codes=None,
working_directory=None,
extra_environment=None,
environment=None,
)
module.create_archive(
@@ -1431,7 +1439,7 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr
borg_local_path='borg',
borg_exit_codes=None,
working_directory=None,
extra_environment=None,
environment=None,
)
flexmock(module).should_receive('execute_command_with_processes').with_args(
create_command,
@@ -1441,7 +1449,7 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr
borg_local_path='borg',
borg_exit_codes=None,
working_directory=None,
extra_environment=None,
environment=None,
)
module.create_archive(
@@ -1472,7 +1480,7 @@ def test_create_archive_with_json_calls_borg_with_json_flag():
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'create', '--json') + REPO_ARCHIVE,
working_directory=None,
extra_environment=None,
environment=None,
borg_local_path='borg',
borg_exit_codes=None,
).and_return('[]')
@@ -1506,7 +1514,7 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_flag():
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'create', '--json') + REPO_ARCHIVE,
working_directory=None,
extra_environment=None,
environment=None,
borg_local_path='borg',
borg_exit_codes=None,
).and_return('[]')
@@ -1547,7 +1555,7 @@ def test_create_archive_calls_borg_with_working_directory():
borg_local_path='borg',
borg_exit_codes=None,
working_directory='/working/dir',
extra_environment=None,
environment=None,
)
module.create_archive(
+3 -3
View File
@@ -370,9 +370,9 @@ def test_delete_archives_calls_borg_delete_with_working_directory():
flexmock(module.borgmatic.borg.repo_delete).should_receive('delete_repository').never()
command = flexmock()
flexmock(module).should_receive('make_delete_command').and_return(command)
extra_environment = flexmock()
environment = flexmock()
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
extra_environment
environment
)
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
'/working/dir'
@@ -380,7 +380,7 @@ def test_delete_archives_calls_borg_delete_with_working_directory():
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
command,
output_log_level=logging.ANSWER,
extra_environment=extra_environment,
environment=environment,
working_directory='/working/dir',
borg_local_path='borg',
borg_exit_codes=None,
+75 -48
View File
@@ -4,6 +4,12 @@ from borgmatic.borg import environment as module
def test_make_environment_with_passcommand_should_call_it_and_set_passphrase_file_descriptor_in_environment():
flexmock(module.os).should_receive('environ').and_return(
{'USER': 'root', 'BORG_PASSCOMMAND': 'nope'}
)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).and_return(None)
flexmock(module.borgmatic.borg.passcommand).should_receive(
'get_passphrase_from_passcommand'
).and_return('passphrase')
@@ -14,132 +20,153 @@ def test_make_environment_with_passcommand_should_call_it_and_set_passphrase_fil
environment = module.make_environment({'encryption_passcommand': 'command'})
assert not environment.get('BORG_PASSCOMMAND')
assert environment.get('BORG_PASSPHRASE') is None
assert environment.get('BORG_PASSCOMMAND') is None
assert environment.get('BORG_PASSPHRASE_FD') == '3'
def test_make_environment_with_passphrase_should_set_environment():
def test_make_environment_with_passphrase_should_set_passphrase_file_descriptor_in_environment():
flexmock(module.os).should_receive('environ').and_return(
{'USER': 'root', 'BORG_PASSPHRASE': 'nope', 'BORG_PASSCOMMAND': 'nope'}
)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.borg.passcommand).should_receive(
'get_passphrase_from_passcommand'
).and_return(None)
flexmock(module.os).should_receive('pipe').never()
flexmock(module.os.environ).should_receive('get').and_return(None)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module.os).should_receive('pipe').and_return((3, 4))
flexmock(module.os).should_receive('write')
flexmock(module.os).should_receive('close')
flexmock(module.os).should_receive('set_inheritable')
environment = module.make_environment({'encryption_passphrase': 'pass'})
assert environment.get('BORG_PASSPHRASE') == 'pass'
assert environment.get('BORG_PASSPHRASE') is None
assert environment.get('BORG_PASSCOMMAND') is None
assert environment.get('BORG_PASSPHRASE_FD') == '3'
def test_make_environment_with_credential_tag_passphrase_should_load_it_and_set_environment():
def test_make_environment_with_credential_tag_passphrase_should_load_it_and_set_passphrase_file_descriptor_in_environment():
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
config = {'encryption_passphrase': '{credential systemd pass}'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential',
).with_args('{credential systemd pass}', config).and_return('pass')
flexmock(module.borgmatic.borg.passcommand).should_receive(
'get_passphrase_from_passcommand'
).and_return(None)
flexmock(module.os).should_receive('pipe').never()
flexmock(module.os.environ).should_receive('get').and_return(None)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).with_args('{credential systemd pass}').and_return('pass')
).never()
flexmock(module.os).should_receive('pipe').and_return((3, 4))
flexmock(module.os).should_receive('write')
flexmock(module.os).should_receive('close')
flexmock(module.os).should_receive('set_inheritable')
environment = module.make_environment({'encryption_passphrase': '{credential systemd pass}'})
environment = module.make_environment(config)
assert environment.get('BORG_PASSPHRASE') == 'pass'
assert environment.get('BORG_PASSPHRASE') is None
assert environment.get('BORG_PASSPHRASE_FD') == '3'
def test_make_environment_with_ssh_command_should_set_environment():
flexmock(module.borgmatic.borg.passcommand).should_receive(
'get_passphrase_from_passcommand'
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).and_return(None)
flexmock(module.os).should_receive('pipe').never()
flexmock(module.os.environ).should_receive('get').and_return(None)
environment = module.make_environment({'ssh_command': 'ssh -C'})
assert environment.get('BORG_RSH') == 'ssh -C'
def test_make_environment_without_configuration_sets_certain_environment_variables():
flexmock(module.borgmatic.borg.passcommand).should_receive(
'get_passphrase_from_passcommand'
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).and_return(None)
flexmock(module.os).should_receive('pipe').never()
flexmock(module.os.environ).should_receive('get').and_return(None)
environment = module.make_environment({})
# Default environment variables.
assert environment == {
'USER': 'root',
'BORG_EXIT_CODES': 'modern',
'BORG_RELOCATED_REPO_ACCESS_IS_OK': 'no',
'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK': 'no',
}
def test_make_environment_without_configuration_does_not_set_certain_environment_variables_if_already_set():
flexmock(module.borgmatic.borg.passcommand).should_receive(
'get_passphrase_from_passcommand'
def test_make_environment_without_configuration_passes_through_default_environment_variables_untouched():
flexmock(module.os).should_receive('environ').and_return(
{
'USER': 'root',
'BORG_RELOCATED_REPO_ACCESS_IS_OK': 'yup',
'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK': 'nah',
}
)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).and_return(None)
flexmock(module.os).should_receive('pipe').never()
flexmock(module.os.environ).should_receive('get').with_args(
'BORG_RELOCATED_REPO_ACCESS_IS_OK'
).and_return('yup')
flexmock(module.os.environ).should_receive('get').with_args(
'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK'
).and_return('nah')
environment = module.make_environment({})
assert environment == {'BORG_EXIT_CODES': 'modern'}
assert environment == {
'USER': 'root',
'BORG_RELOCATED_REPO_ACCESS_IS_OK': 'yup',
'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK': 'nah',
'BORG_EXIT_CODES': 'modern',
}
def test_make_environment_with_relocated_repo_access_true_should_set_environment_yes():
flexmock(module.borgmatic.borg.passcommand).should_receive(
'get_passphrase_from_passcommand'
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).and_return(None)
flexmock(module.os).should_receive('pipe').never()
flexmock(module.os.environ).should_receive('get').and_return(None)
environment = module.make_environment({'relocated_repo_access_is_ok': True})
assert environment.get('BORG_RELOCATED_REPO_ACCESS_IS_OK') == 'yes'
def test_make_environment_with_relocated_repo_access_false_should_set_environment_no():
flexmock(module.borgmatic.borg.passcommand).should_receive(
'get_passphrase_from_passcommand'
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).and_return(None)
flexmock(module.os).should_receive('pipe').never()
flexmock(module.os.environ).should_receive('get').and_return(None)
environment = module.make_environment({'relocated_repo_access_is_ok': False})
assert environment.get('BORG_RELOCATED_REPO_ACCESS_IS_OK') == 'no'
def test_make_environment_check_i_know_what_i_am_doing_true_should_set_environment_YES():
flexmock(module.borgmatic.borg.passcommand).should_receive(
'get_passphrase_from_passcommand'
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).and_return(None)
flexmock(module.os).should_receive('pipe').never()
flexmock(module.os.environ).should_receive('get').and_return(None)
environment = module.make_environment({'check_i_know_what_i_am_doing': True})
assert environment.get('BORG_CHECK_I_KNOW_WHAT_I_AM_DOING') == 'YES'
def test_make_environment_check_i_know_what_i_am_doing_false_should_set_environment_NO():
flexmock(module.borgmatic.borg.passcommand).should_receive(
'get_passphrase_from_passcommand'
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).and_return(None)
flexmock(module.os).should_receive('pipe').never()
flexmock(module.os.environ).should_receive('get').and_return(None)
environment = module.make_environment({'check_i_know_what_i_am_doing': False})
assert environment.get('BORG_CHECK_I_KNOW_WHAT_I_AM_DOING') == 'NO'
def test_make_environment_with_integer_variable_value():
flexmock(module.borgmatic.borg.passcommand).should_receive(
'get_passphrase_from_passcommand'
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).and_return(None)
flexmock(module.os).should_receive('pipe').never()
flexmock(module.os.environ).should_receive('get').and_return(None)
environment = module.make_environment({'borg_files_cache_ttl': 40})
assert environment.get('BORG_FILES_CACHE_TTL') == '40'
+1 -1
View File
@@ -22,7 +22,7 @@ def insert_execute_command_mock(
command,
output_file=output_file,
output_log_level=module.logging.ANSWER,
extra_environment=None,
environment=None,
working_directory=working_directory,
borg_local_path=command[0],
borg_exit_codes=borg_exit_codes,
+1 -1
View File
@@ -23,7 +23,7 @@ def insert_execute_command_mock(
command,
output_file=None if capture else module.DO_NOT_CAPTURE,
output_log_level=output_log_level,
extra_environment=None,
environment=None,
working_directory=working_directory,
borg_local_path=borg_local_path,
borg_exit_codes=borg_exit_codes,
+4 -4
View File
@@ -12,7 +12,7 @@ def insert_execute_command_mock(command, destination_path=None, borg_exit_codes=
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
command,
extra_environment=None,
environment=None,
working_directory=destination_path,
borg_local_path=command[0],
borg_exit_codes=borg_exit_codes,
@@ -587,7 +587,7 @@ def test_extract_archive_calls_borg_with_progress_parameter():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'extract', '--progress', 'repo::archive'),
output_file=module.DO_NOT_CAPTURE,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -639,7 +639,7 @@ def test_extract_archive_calls_borg_with_stdout_parameter_and_returns_process():
('borg', 'extract', '--stdout', 'repo::archive'),
output_file=module.subprocess.PIPE,
run_to_completion=False,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -674,7 +674,7 @@ def test_extract_archive_skips_abspath_for_remote_repository():
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command').with_args(
('borg', 'extract', 'server:repo::archive'),
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
+42
View File
@@ -285,3 +285,45 @@ def test_warn_for_aggressive_archive_flags_with_glob_archives_and_json_missing_a
flexmock(module.logger).should_receive('warning').never()
module.warn_for_aggressive_archive_flags(('borg', '--glob-archives', 'foo*'), '{}')
def test_omit_flag_removes_flag_from_arguments():
assert module.omit_flag(('borg', 'create', '--flag', '--other'), '--flag') == (
'borg',
'create',
'--other',
)
def test_omit_flag_without_flag_present_passes_through_arguments():
assert module.omit_flag(('borg', 'create', '--other'), '--flag') == (
'borg',
'create',
'--other',
)
def test_omit_flag_and_value_removes_flag_and_value_from_arguments():
assert module.omit_flag_and_value(
('borg', 'create', '--flag', 'value', '--other'), '--flag'
) == (
'borg',
'create',
'--other',
)
def test_omit_flag_and_value_with_equals_sign_removes_flag_and_value_from_arguments():
assert module.omit_flag_and_value(('borg', 'create', '--flag=value', '--other'), '--flag') == (
'borg',
'create',
'--other',
)
def test_omit_flag_and_value_without_flag_present_passes_through_arguments():
assert module.omit_flag_and_value(('borg', 'create', '--other'), '--flag') == (
'borg',
'create',
'--other',
)
+2 -2
View File
@@ -514,7 +514,7 @@ def test_display_archives_info_calls_borg_with_working_directory():
)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
full_command=object,
extra_environment=object,
environment=object,
working_directory='/working/dir',
borg_local_path=object,
borg_exit_codes=object,
@@ -523,7 +523,7 @@ def test_display_archives_info_calls_borg_with_working_directory():
flexmock(module).should_receive('execute_command').with_args(
full_command=object,
output_log_level=object,
extra_environment=object,
environment=object,
working_directory='/working/dir',
borg_local_path=object,
borg_exit_codes=object,
+12 -12
View File
@@ -353,7 +353,7 @@ def test_list_archive_calls_borg_with_flags():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'list', 'repo::archive'),
output_log_level=module.borgmatic.logger.ANSWER,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -419,7 +419,7 @@ def test_list_archive_calls_borg_with_local_path():
flexmock(module).should_receive('execute_command').with_args(
('borg2', 'list', 'repo::archive'),
output_log_level=module.borgmatic.logger.ANSWER,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg2',
borg_exit_codes=None,
@@ -469,7 +469,7 @@ def test_list_archive_calls_borg_using_exit_codes():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'list', 'repo::archive'),
output_log_level=module.borgmatic.logger.ANSWER,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=borg_exit_codes,
@@ -507,7 +507,7 @@ def test_list_archive_calls_borg_multiple_times_with_find_paths():
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'list', 'repo'),
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -520,7 +520,7 @@ def test_list_archive_calls_borg_multiple_times_with_find_paths():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'list', 'repo::archive1') + glob_paths,
output_log_level=module.borgmatic.logger.ANSWER,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -528,7 +528,7 @@ def test_list_archive_calls_borg_multiple_times_with_find_paths():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'list', 'repo::archive2') + glob_paths,
output_log_level=module.borgmatic.logger.ANSWER,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -576,7 +576,7 @@ def test_list_archive_calls_borg_with_archive():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'list', 'repo::archive'),
output_log_level=module.borgmatic.logger.ANSWER,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -699,7 +699,7 @@ def test_list_archive_with_archive_ignores_archive_filter_flag(
flexmock(module).should_receive('execute_command').with_args(
('borg', 'list', 'repo::archive'),
output_log_level=module.borgmatic.logger.ANSWER,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -759,7 +759,7 @@ def test_list_archive_with_find_paths_allows_archive_filter_flag_but_only_passes
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'repo-list', '--repo', 'repo'),
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -808,7 +808,7 @@ def test_list_archive_with_find_paths_allows_archive_filter_flag_but_only_passes
flexmock(module).should_receive('execute_command').with_args(
('borg', 'list', '--repo', 'repo', 'archive1') + glob_paths,
output_log_level=module.borgmatic.logger.ANSWER,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -816,7 +816,7 @@ def test_list_archive_with_find_paths_allows_archive_filter_flag_but_only_passes
flexmock(module).should_receive('execute_command').with_args(
('borg', 'list', '--repo', 'repo', 'archive2') + glob_paths,
output_log_level=module.borgmatic.logger.ANSWER,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -875,7 +875,7 @@ def test_list_archive_calls_borg_with_working_directory():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'list', 'repo::archive'),
output_log_level=module.borgmatic.logger.ANSWER,
extra_environment=None,
environment=None,
working_directory='/working/dir',
borg_local_path='borg',
borg_exit_codes=None,
+3 -3
View File
@@ -14,7 +14,7 @@ def insert_execute_command_mock(command, working_directory=None, borg_exit_codes
)
flexmock(module).should_receive('execute_command').with_args(
command,
extra_environment=None,
environment=None,
working_directory=working_directory,
borg_local_path=command[0],
borg_exit_codes=borg_exit_codes,
@@ -262,7 +262,7 @@ def test_mount_archive_calls_borg_with_foreground_parameter():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'mount', '--foreground', 'repo::archive', '/mnt'),
output_file=module.DO_NOT_CAPTURE,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -337,7 +337,7 @@ def test_mount_archive_with_date_based_matching_calls_borg_with_date_based_flags
'repo',
'/mnt',
),
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
+11 -58
View File
@@ -3,26 +3,13 @@ from flexmock import flexmock
from borgmatic.borg import passcommand as module
def test_run_passcommand_with_passphrase_configured_bails():
module.run_passcommand.cache_clear()
flexmock(module.borgmatic.execute).should_receive('execute_command_and_capture_output').never()
assert (
module.run_passcommand('passcommand', passphrase_configured=True, working_directory=None)
is None
)
def test_run_passcommand_without_passphrase_configured_executes_passcommand():
def test_run_passcommand_does_not_raise():
module.run_passcommand.cache_clear()
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).and_return('passphrase').once()
).and_return('passphrase')
assert (
module.run_passcommand('passcommand', passphrase_configured=False, working_directory=None)
== 'passphrase'
)
assert module.run_passcommand('passcommand', working_directory=None) == 'passphrase'
def test_get_passphrase_from_passcommand_with_configured_passcommand_runs_it():
@@ -30,9 +17,9 @@ def test_get_passphrase_from_passcommand_with_configured_passcommand_runs_it():
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
'/working'
)
flexmock(module).should_receive('run_passcommand').with_args(
'command', False, '/working'
).and_return('passphrase').once()
flexmock(module).should_receive('run_passcommand').with_args('command', '/working').and_return(
'passphrase'
).once()
assert (
module.get_passphrase_from_passcommand(
@@ -42,38 +29,10 @@ def test_get_passphrase_from_passcommand_with_configured_passcommand_runs_it():
)
def test_get_passphrase_from_passcommand_with_configured_passphrase_and_passcommand_detects_passphrase():
module.run_passcommand.cache_clear()
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
'/working'
)
flexmock(module).should_receive('run_passcommand').with_args(
'command', True, '/working'
).and_return(None).once()
def test_get_passphrase_from_passcommand_without_configured_passcommand_bails():
flexmock(module).should_receive('run_passcommand').never()
assert (
module.get_passphrase_from_passcommand(
{'encryption_passphrase': 'passphrase', 'encryption_passcommand': 'command'},
)
is None
)
def test_get_passphrase_from_passcommand_with_configured_blank_passphrase_and_passcommand_detects_passphrase():
module.run_passcommand.cache_clear()
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
'/working'
)
flexmock(module).should_receive('run_passcommand').with_args(
'command', True, '/working'
).and_return(None).once()
assert (
module.get_passphrase_from_passcommand(
{'encryption_passphrase': '', 'encryption_passcommand': 'command'},
)
is None
)
assert module.get_passphrase_from_passcommand({}) is None
def test_run_passcommand_caches_passcommand_after_first_call():
@@ -82,11 +41,5 @@ def test_run_passcommand_caches_passcommand_after_first_call():
'execute_command_and_capture_output'
).and_return('passphrase').once()
assert (
module.run_passcommand('passcommand', passphrase_configured=False, working_directory=None)
== 'passphrase'
)
assert (
module.run_passcommand('passcommand', passphrase_configured=False, working_directory=None)
== 'passphrase'
)
assert module.run_passcommand('passcommand', working_directory=None) == 'passphrase'
assert module.run_passcommand('passcommand', working_directory=None) == 'passphrase'
+2 -2
View File
@@ -17,7 +17,7 @@ def insert_execute_command_mock(
flexmock(module).should_receive('execute_command').with_args(
prune_command,
output_log_level=output_log_level,
extra_environment=None,
environment=None,
working_directory=working_directory,
borg_local_path=prune_command[0],
borg_exit_codes=borg_exit_codes,
@@ -497,7 +497,7 @@ def test_prune_archives_with_date_based_matching_calls_borg_with_date_based_flag
'repo',
),
output_log_level=logging.INFO,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
+1 -1
View File
@@ -36,7 +36,7 @@ def insert_repo_create_command_mock(
flexmock(module).should_receive('execute_command').with_args(
repo_create_command,
output_file=module.DO_NOT_CAPTURE,
extra_environment=None,
environment=None,
working_directory=working_directory,
borg_local_path=repo_create_command[0],
borg_exit_codes=borg_exit_codes,
+4 -4
View File
@@ -290,7 +290,7 @@ def test_delete_repository_with_defaults_does_not_capture_output():
command,
output_log_level=module.logging.ANSWER,
output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
extra_environment=object,
environment=object,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -319,7 +319,7 @@ def test_delete_repository_with_force_captures_output():
command,
output_log_level=module.logging.ANSWER,
output_file=None,
extra_environment=object,
environment=object,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -348,7 +348,7 @@ def test_delete_repository_with_cache_only_captures_output():
command,
output_log_level=module.logging.ANSWER,
output_file=None,
extra_environment=object,
environment=object,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -379,7 +379,7 @@ def test_delete_repository_calls_borg_with_working_directory():
command,
output_log_level=module.logging.ANSWER,
output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
extra_environment=object,
environment=object,
working_directory='/working/dir',
borg_local_path='borg',
borg_exit_codes=None,
+25 -25
View File
@@ -24,7 +24,7 @@ def test_display_repository_info_calls_borg_with_flags():
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'repo-info', '--json', '--repo', 'repo'),
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -33,7 +33,7 @@ def test_display_repository_info_calls_borg_with_flags():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'repo-info', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -60,7 +60,7 @@ def test_display_repository_info_without_borg_features_calls_borg_with_info_sub_
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'repo-info', '--json', 'repo'),
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -69,7 +69,7 @@ def test_display_repository_info_without_borg_features_calls_borg_with_info_sub_
flexmock(module).should_receive('execute_command').with_args(
('borg', 'info', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -101,7 +101,7 @@ def test_display_repository_info_with_log_info_calls_borg_with_info_flag():
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'repo-info', '--info', '--json', '--repo', 'repo'),
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -110,7 +110,7 @@ def test_display_repository_info_with_log_info_calls_borg_with_info_flag():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'repo-info', '--info', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -142,7 +142,7 @@ def test_display_repository_info_with_log_info_and_json_suppresses_most_borg_out
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'repo-info', '--json', '--repo', 'repo'),
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -178,7 +178,7 @@ def test_display_repository_info_with_log_debug_calls_borg_with_debug_flag():
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'repo-info', '--debug', '--show-rc', '--json', '--repo', 'repo'),
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -187,7 +187,7 @@ def test_display_repository_info_with_log_debug_calls_borg_with_debug_flag():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'repo-info', '--debug', '--show-rc', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -220,7 +220,7 @@ def test_display_repository_info_with_log_debug_and_json_suppresses_most_borg_ou
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'repo-info', '--json', '--repo', 'repo'),
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -256,7 +256,7 @@ def test_display_repository_info_with_json_calls_borg_with_json_flag():
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'repo-info', '--json', '--repo', 'repo'),
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -291,7 +291,7 @@ def test_display_repository_info_with_local_path_calls_borg_via_local_path():
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg1', 'repo-info', '--json', '--repo', 'repo'),
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -300,7 +300,7 @@ def test_display_repository_info_with_local_path_calls_borg_via_local_path():
flexmock(module).should_receive('execute_command').with_args(
('borg1', 'repo-info', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg1',
borg_exit_codes=None,
@@ -334,7 +334,7 @@ def test_display_repository_info_with_exit_codes_calls_borg_using_them():
borg_exit_codes = flexmock()
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'repo-info', '--json', '--repo', 'repo'),
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=borg_exit_codes,
@@ -343,7 +343,7 @@ def test_display_repository_info_with_exit_codes_calls_borg_using_them():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'repo-info', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=borg_exit_codes,
@@ -375,7 +375,7 @@ def test_display_repository_info_with_remote_path_calls_borg_with_remote_path_fl
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'repo-info', '--remote-path', 'borg1', '--json', '--repo', 'repo'),
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -384,7 +384,7 @@ def test_display_repository_info_with_remote_path_calls_borg_with_remote_path_fl
flexmock(module).should_receive('execute_command').with_args(
('borg', 'repo-info', '--remote-path', 'borg1', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -417,7 +417,7 @@ def test_display_repository_info_with_umask_calls_borg_with_umask_flags():
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'repo-info', '--umask', '077', '--json', '--repo', 'repo'),
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -426,7 +426,7 @@ def test_display_repository_info_with_umask_calls_borg_with_umask_flags():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'repo-info', '--umask', '077', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -462,7 +462,7 @@ def test_display_repository_info_with_log_json_calls_borg_with_log_json_flags():
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'repo-info', '--log-json', '--json', '--repo', 'repo'),
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -471,7 +471,7 @@ def test_display_repository_info_with_log_json_calls_borg_with_log_json_flags():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'repo-info', '--log-json', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -504,7 +504,7 @@ def test_display_repository_info_with_lock_wait_calls_borg_with_lock_wait_flags(
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'repo-info', '--lock-wait', '5', '--json', '--repo', 'repo'),
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -513,7 +513,7 @@ def test_display_repository_info_with_lock_wait_calls_borg_with_lock_wait_flags(
flexmock(module).should_receive('execute_command').with_args(
('borg', 'repo-info', '--lock-wait', '5', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -547,7 +547,7 @@ def test_display_repository_info_calls_borg_with_working_directory():
)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'repo-info', '--json', '--repo', 'repo'),
extra_environment=None,
environment=None,
working_directory='/working/dir',
borg_local_path='borg',
borg_exit_codes=None,
@@ -556,7 +556,7 @@ def test_display_repository_info_calls_borg_with_working_directory():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'repo-info', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
extra_environment=None,
environment=None,
working_directory='/working/dir',
borg_local_path='borg',
borg_exit_codes=None,
+13 -13
View File
@@ -39,7 +39,7 @@ def test_resolve_archive_name_calls_borg_with_flags():
('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
borg_local_path='borg',
borg_exit_codes=None,
extra_environment=None,
environment=None,
working_directory=None,
).and_return(expected_archive + '\n')
@@ -61,7 +61,7 @@ def test_resolve_archive_name_with_log_info_calls_borg_without_info_flag():
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -86,7 +86,7 @@ def test_resolve_archive_name_with_log_debug_calls_borg_without_debug_flag():
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -111,7 +111,7 @@ def test_resolve_archive_name_with_local_path_calls_borg_via_local_path():
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg1', 'list') + BORG_LIST_LATEST_ARGUMENTS,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg1',
borg_exit_codes=None,
@@ -137,7 +137,7 @@ def test_resolve_archive_name_with_exit_codes_calls_borg_using_them():
borg_exit_codes = flexmock()
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=borg_exit_codes,
@@ -161,7 +161,7 @@ def test_resolve_archive_name_with_remote_path_calls_borg_with_remote_path_flags
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'list', '--remote-path', 'borg1') + BORG_LIST_LATEST_ARGUMENTS,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -186,7 +186,7 @@ def test_resolve_archive_name_with_umask_calls_borg_with_umask_flags():
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'list', '--umask', '077') + BORG_LIST_LATEST_ARGUMENTS,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -209,7 +209,7 @@ def test_resolve_archive_name_without_archives_raises():
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -232,7 +232,7 @@ def test_resolve_archive_name_with_log_json_calls_borg_with_log_json_flags():
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'list', '--log-json') + BORG_LIST_LATEST_ARGUMENTS,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -257,7 +257,7 @@ def test_resolve_archive_name_with_lock_wait_calls_borg_with_lock_wait_flags():
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'list', '--lock-wait', 'okay') + BORG_LIST_LATEST_ARGUMENTS,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -285,7 +285,7 @@ def test_resolve_archive_name_calls_borg_with_working_directory():
('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
borg_local_path='borg',
borg_exit_codes=None,
extra_environment=None,
environment=None,
working_directory='/working/dir',
).and_return(expected_archive + '\n')
@@ -773,7 +773,7 @@ def test_list_repository_calls_borg_with_working_directory():
)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
full_command=object,
extra_environment=object,
environment=object,
working_directory='/working/dir',
borg_local_path=object,
borg_exit_codes=object,
@@ -782,7 +782,7 @@ def test_list_repository_calls_borg_with_working_directory():
flexmock(module).should_receive('execute_command').with_args(
full_command=object,
output_log_level=object,
extra_environment=object,
environment=object,
working_directory='/working/dir',
borg_local_path=object,
borg_exit_codes=object,
+18 -18
View File
@@ -21,7 +21,7 @@ def test_transfer_archives_calls_borg_with_flags():
('borg', 'transfer', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
output_file=None,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -55,7 +55,7 @@ def test_transfer_archives_with_dry_run_calls_borg_with_dry_run_flag():
('borg', 'transfer', '--repo', 'repo', '--dry-run'),
output_log_level=module.borgmatic.logger.ANSWER,
output_file=None,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -86,7 +86,7 @@ def test_transfer_archives_with_log_info_calls_borg_with_info_flag():
('borg', 'transfer', '--info', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
output_file=None,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -117,7 +117,7 @@ def test_transfer_archives_with_log_debug_calls_borg_with_debug_flag():
('borg', 'transfer', '--debug', '--show-rc', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
output_file=None,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -151,7 +151,7 @@ def test_transfer_archives_with_archive_calls_borg_with_match_archives_flag():
('borg', 'transfer', '--match-archives', 'archive', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
output_file=None,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -184,7 +184,7 @@ def test_transfer_archives_with_match_archives_calls_borg_with_match_archives_fl
('borg', 'transfer', '--match-archives', 'sh:foo*', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
output_file=None,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -217,7 +217,7 @@ def test_transfer_archives_with_archive_name_format_calls_borg_with_match_archiv
('borg', 'transfer', '--match-archives', 'sh:bar-*', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
output_file=None,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -248,7 +248,7 @@ def test_transfer_archives_with_local_path_calls_borg_via_local_path():
('borg2', 'transfer', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
output_file=None,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg2',
borg_exit_codes=None,
@@ -281,7 +281,7 @@ def test_transfer_archives_with_exit_codes_calls_borg_using_them():
('borg', 'transfer', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
output_file=None,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=borg_exit_codes,
@@ -315,7 +315,7 @@ def test_transfer_archives_with_remote_path_calls_borg_with_remote_path_flags():
('borg', 'transfer', '--remote-path', 'borg2', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
output_file=None,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -349,7 +349,7 @@ def test_transfer_archives_with_umask_calls_borg_with_umask_flags():
('borg', 'transfer', '--umask', '077', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
output_file=None,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -383,7 +383,7 @@ def test_transfer_archives_with_log_json_calls_borg_with_log_json_flags():
('borg', 'transfer', '--log-json', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
output_file=None,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -418,7 +418,7 @@ def test_transfer_archives_with_lock_wait_calls_borg_with_lock_wait_flags():
('borg', 'transfer', '--lock-wait', '5', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
output_file=None,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -449,7 +449,7 @@ def test_transfer_archives_with_progress_calls_borg_with_progress_flag():
('borg', 'transfer', '--progress', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
output_file=module.DO_NOT_CAPTURE,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -484,7 +484,7 @@ def test_transfer_archives_passes_through_arguments_to_borg(argument_name):
('borg', 'transfer', flag_name, 'value', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
output_file=None,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -521,7 +521,7 @@ def test_transfer_archives_with_source_repository_calls_borg_with_other_repo_fla
('borg', 'transfer', '--repo', 'repo', '--other-repo', 'other'),
output_log_level=module.borgmatic.logger.ANSWER,
output_file=None,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -566,7 +566,7 @@ def test_transfer_archives_with_date_based_matching_calls_borg_with_date_based_f
),
output_log_level=module.borgmatic.logger.ANSWER,
output_file=None,
extra_environment=None,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
@@ -605,7 +605,7 @@ def test_transfer_archives_calls_borg_with_working_directory():
('borg', 'transfer', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
output_file=None,
extra_environment=None,
environment=None,
working_directory='/working/dir',
borg_local_path='borg',
borg_exit_codes=None,
+1 -1
View File
@@ -23,7 +23,7 @@ def insert_execute_command_and_capture_output_mock(
)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
command,
extra_environment=None,
environment=None,
working_directory=working_directory,
borg_local_path=borg_local_path,
borg_exit_codes=borg_exit_codes,
@@ -0,0 +1,74 @@
import io
import sys
import pytest
from flexmock import flexmock
from borgmatic.hooks.credential import container as module
@pytest.mark.parametrize('credential_parameters', ((), ('foo', 'bar')))
def test_load_credential_with_invalid_credential_parameters_raises(credential_parameters):
with pytest.raises(ValueError):
module.load_credential(
hook_config={}, config={}, credential_parameters=credential_parameters
)
def test_load_credential_with_invalid_secret_name_raises():
with pytest.raises(ValueError):
module.load_credential(
hook_config={}, config={}, credential_parameters=('this is invalid',)
)
def test_load_credential_reads_named_secret_from_file():
credential_stream = io.StringIO('password')
credential_stream.name = '/run/secrets/mysecret'
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('/run/secrets/mysecret').and_return(credential_stream)
assert (
module.load_credential(hook_config={}, config={}, credential_parameters=('mysecret',))
== 'password'
)
def test_load_credential_with_custom_secrets_directory_looks_there_for_secret_file():
config = {'container': {'secrets_directory': '/secrets'}}
credential_stream = io.StringIO('password')
credential_stream.name = '/secrets/mysecret'
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('/secrets/mysecret').and_return(credential_stream)
assert (
module.load_credential(
hook_config=config['container'], config=config, credential_parameters=('mysecret',)
)
== 'password'
)
def test_load_credential_with_custom_secrets_directory_prefixes_it_with_working_directory():
config = {'container': {'secrets_directory': 'secrets'}, 'working_directory': '/working'}
credential_stream = io.StringIO('password')
credential_stream.name = '/working/secrets/mysecret'
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('/working/secrets/mysecret').and_return(
credential_stream
)
assert (
module.load_credential(
hook_config=config['container'], config=config, credential_parameters=('mysecret',)
)
== 'password'
)
def test_load_credential_with_file_not_found_error_raises():
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('/run/secrets/mysecret').and_raise(FileNotFoundError)
with pytest.raises(ValueError):
module.load_credential(hook_config={}, config={}, credential_parameters=('mysecret',))
+68
View File
@@ -0,0 +1,68 @@
import io
import sys
import pytest
from flexmock import flexmock
from borgmatic.hooks.credential import file as module
@pytest.mark.parametrize('credential_parameters', ((), ('foo', 'bar')))
def test_load_credential_with_invalid_credential_parameters_raises(credential_parameters):
with pytest.raises(ValueError):
module.load_credential(
hook_config={}, config={}, credential_parameters=credential_parameters
)
def test_load_credential_with_invalid_credential_name_raises():
with pytest.raises(ValueError):
module.load_credential(
hook_config={}, config={}, credential_parameters=('this is invalid',)
)
def test_load_credential_reads_named_credential_from_file():
credential_stream = io.StringIO('password')
credential_stream.name = '/credentials/mycredential'
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('/credentials/mycredential').and_return(
credential_stream
)
assert (
module.load_credential(
hook_config={}, config={}, credential_parameters=('/credentials/mycredential',)
)
== 'password'
)
def test_load_credential_reads_named_credential_from_file_using_working_directory():
credential_stream = io.StringIO('password')
credential_stream.name = '/working/credentials/mycredential'
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('/working/credentials/mycredential').and_return(
credential_stream
)
assert (
module.load_credential(
hook_config={},
config={'working_directory': '/working'},
credential_parameters=('credentials/mycredential',),
)
== 'password'
)
def test_load_credential_with_file_not_found_error_raises():
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('/credentials/mycredential').and_raise(
FileNotFoundError
)
with pytest.raises(ValueError):
module.load_credential(
hook_config={}, config={}, credential_parameters=('/credentials/mycredential',)
)
@@ -0,0 +1,80 @@
import pytest
from flexmock import flexmock
from borgmatic.hooks.credential import keepassxc as module
@pytest.mark.parametrize('credential_parameters', ((), ('foo',), ('foo', 'bar', 'baz')))
def test_load_credential_with_invalid_credential_parameters_raises(credential_parameters):
flexmock(module.borgmatic.execute).should_receive('execute_command_and_capture_output').never()
with pytest.raises(ValueError):
module.load_credential(
hook_config={}, config={}, credential_parameters=credential_parameters
)
def test_load_credential_with_missing_database_raises():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.execute).should_receive('execute_command_and_capture_output').never()
with pytest.raises(ValueError):
module.load_credential(
hook_config={}, config={}, credential_parameters=('database.kdbx', 'mypassword')
)
def test_load_credential_with_present_database_fetches_password_from_keepassxc():
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).with_args(
(
'keepassxc-cli',
'show',
'--show-protected',
'--attributes',
'Password',
'database.kdbx',
'mypassword',
)
).and_return(
'password'
).once()
assert (
module.load_credential(
hook_config={}, config={}, credential_parameters=('database.kdbx', 'mypassword')
)
== 'password'
)
def test_load_credential_with_custom_keepassxc_cli_command_calls_it():
config = {'keepassxc': {'keepassxc_cli_command': '/usr/local/bin/keepassxc-cli --some-option'}}
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).with_args(
(
'/usr/local/bin/keepassxc-cli',
'--some-option',
'show',
'--show-protected',
'--attributes',
'Password',
'database.kdbx',
'mypassword',
)
).and_return(
'password'
).once()
assert (
module.load_credential(
hook_config=config['keepassxc'],
config=config,
credential_parameters=('database.kdbx', 'mypassword'),
)
== 'password'
)
+54 -11
View File
@@ -4,39 +4,82 @@ from flexmock import flexmock
from borgmatic.hooks.credential import parse as module
def test_resolve_credential_passes_through_string_without_credential_tag():
def test_hash_adapter_is_always_equal():
assert module.Hash_adapter({1: 2}) == module.Hash_adapter({3: 4})
def test_hash_adapter_alwaysh_hashes_the_same():
assert hash(module.Hash_adapter({1: 2})) == hash(module.Hash_adapter({3: 4}))
def test_cache_ignoring_unhashable_arguments_caches_arguments_after_first_call():
hashable = 3
unhashable = {1, 2}
calls = 0
@module.cache_ignoring_unhashable_arguments
def function(first, second, third):
nonlocal calls
calls += 1
assert first == hashable
assert second == unhashable
assert third == unhashable
return first
assert function(hashable, unhashable, third=unhashable) == hashable
assert calls == 1
assert function(hashable, unhashable, third=unhashable) == hashable
assert calls == 1
def test_resolve_credential_passes_through_string_without_credential():
module.resolve_credential.cache_clear()
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').never()
assert module.resolve_credential('{no credentials here}') == '{no credentials here}'
assert module.resolve_credential('{no credentials here}', config={}) == '{no credentials here}'
def test_resolve_credential_passes_through_none():
module.resolve_credential.cache_clear()
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').never()
assert module.resolve_credential(None) is None
assert module.resolve_credential(None, config={}) is None
@pytest.mark.parametrize('invalid_value', ('{credential}', '{credential }', '{credential systemd}'))
def test_resolve_credential_with_invalid_credential_tag_raises(invalid_value):
def test_resolve_credential_with_invalid_credential_raises(invalid_value):
module.resolve_credential.cache_clear()
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').never()
with pytest.raises(ValueError):
module.resolve_credential(invalid_value)
module.resolve_credential(invalid_value, config={})
def test_resolve_credential_with_valid_credential_tag_loads_credential():
def test_resolve_credential_with_valid_credential_loads_credential():
module.resolve_credential.cache_clear()
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').with_args(
'load_credential',
{},
'systemd',
'mycredential',
('mycredential',),
).and_return('result').once()
assert module.resolve_credential('{credential systemd mycredential}') == 'result'
assert module.resolve_credential('{credential systemd mycredential}', config={}) == 'result'
def test_resolve_credential_with_valid_credential_and_quoted_parameters_loads_credential():
module.resolve_credential.cache_clear()
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').with_args(
'load_credential',
{},
'systemd',
('my credential',),
).and_return('result').once()
assert module.resolve_credential('{credential systemd "my credential"}', config={}) == 'result'
def test_resolve_credential_caches_credential_after_first_call():
@@ -45,8 +88,8 @@ def test_resolve_credential_caches_credential_after_first_call():
'load_credential',
{},
'systemd',
'mycredential',
('mycredential',),
).and_return('result').once()
assert module.resolve_credential('{credential systemd mycredential}') == 'result'
assert module.resolve_credential('{credential systemd mycredential}') == 'result'
assert module.resolve_credential('{credential systemd mycredential}', config={}) == 'result'
assert module.resolve_credential('{credential systemd mycredential}', config={}) == 'result'
+16 -4
View File
@@ -7,13 +7,23 @@ from flexmock import flexmock
from borgmatic.hooks.credential import systemd as module
@pytest.mark.parametrize('credential_parameters', ((), ('foo', 'bar')))
def test_load_credential_with_invalid_credential_parameters_raises(credential_parameters):
flexmock(module.os.environ).should_receive('get').never()
with pytest.raises(ValueError):
module.load_credential(
hook_config={}, config={}, credential_parameters=credential_parameters
)
def test_load_credential_without_credentials_directory_raises():
flexmock(module.os.environ).should_receive('get').with_args('CREDENTIALS_DIRECTORY').and_return(
None
)
with pytest.raises(ValueError):
module.load_credential(hook_config={}, config={}, credential_name='mycredential')
module.load_credential(hook_config={}, config={}, credential_parameters=('mycredential',))
def test_load_credential_with_invalid_credential_name_raises():
@@ -22,7 +32,9 @@ def test_load_credential_with_invalid_credential_name_raises():
)
with pytest.raises(ValueError):
module.load_credential(hook_config={}, config={}, credential_name='../../my!@#$credential')
module.load_credential(
hook_config={}, config={}, credential_parameters=('../../my!@#$credential',)
)
def test_load_credential_reads_named_credential_from_file():
@@ -35,7 +47,7 @@ def test_load_credential_reads_named_credential_from_file():
builtins.should_receive('open').with_args('/var/mycredential').and_return(credential_stream)
assert (
module.load_credential(hook_config={}, config={}, credential_name='mycredential')
module.load_credential(hook_config={}, config={}, credential_parameters=('mycredential',))
== 'password'
)
@@ -48,4 +60,4 @@ def test_load_credential_with_file_not_found_error_raises():
builtins.should_receive('open').with_args('/var/mycredential').and_raise(FileNotFoundError)
with pytest.raises(ValueError):
module.load_credential(hook_config={}, config={}, credential_name='mycredential')
module.load_credential(hook_config={}, config={}, credential_parameters=('mycredential',))
+127 -3
View File
@@ -1,7 +1,7 @@
import pytest
from flexmock import flexmock
from borgmatic.borg.pattern import Pattern, Pattern_style, Pattern_type
from borgmatic.borg.pattern import Pattern, Pattern_source, Pattern_style, Pattern_type
from borgmatic.hooks.data_source import btrfs as module
@@ -52,9 +52,14 @@ def test_get_subvolume_mount_points_with_findmnt_json_missing_filesystems_errors
def test_get_subvolumes_collects_subvolumes_matching_patterns():
flexmock(module).should_receive('get_subvolume_mount_points').and_return(('/mnt1', '/mnt2'))
contained_pattern = Pattern(
'/mnt1',
type=Pattern_type.ROOT,
source=Pattern_source.CONFIG,
)
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_patterns'
).with_args('/mnt1', object).and_return((Pattern('/mnt1'),))
).with_args('/mnt1', object).and_return((contained_pattern,))
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_patterns'
).with_args('/mnt2', object).and_return(())
@@ -66,7 +71,69 @@ def test_get_subvolumes_collects_subvolumes_matching_patterns():
Pattern('/mnt1'),
Pattern('/mnt3'),
],
) == (module.Subvolume('/mnt1', contained_patterns=(Pattern('/mnt1'),)),)
) == (module.Subvolume('/mnt1', contained_patterns=(contained_pattern,)),)
def test_get_subvolumes_skips_non_root_patterns():
flexmock(module).should_receive('get_subvolume_mount_points').and_return(('/mnt1', '/mnt2'))
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_patterns'
).with_args('/mnt1', object).and_return(
(
Pattern(
'/mnt1',
type=Pattern_type.EXCLUDE,
source=Pattern_source.CONFIG,
),
)
)
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_patterns'
).with_args('/mnt2', object).and_return(())
assert (
module.get_subvolumes(
'btrfs',
'findmnt',
patterns=[
Pattern('/mnt1'),
Pattern('/mnt3'),
],
)
== ()
)
def test_get_subvolumes_skips_non_config_patterns():
flexmock(module).should_receive('get_subvolume_mount_points').and_return(('/mnt1', '/mnt2'))
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_patterns'
).with_args('/mnt1', object).and_return(
(
Pattern(
'/mnt1',
type=Pattern_type.ROOT,
source=Pattern_source.HOOK,
),
)
)
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_patterns'
).with_args('/mnt2', object).and_return(())
assert (
module.get_subvolumes(
'btrfs',
'findmnt',
patterns=[
Pattern('/mnt1'),
Pattern('/mnt3'),
],
)
== ()
)
def test_get_subvolumes_without_patterns_collects_all_subvolumes():
@@ -520,6 +587,18 @@ def test_remove_data_source_dumps_deletes_snapshots():
flexmock(module).should_receive('delete_snapshot').with_args(
'btrfs', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2'
).never()
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol1/.borgmatic-1234'
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol1/.borgmatic-5678'
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol2/.borgmatic-1234'
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol2/.borgmatic-5678'
).and_return(True)
flexmock(module.shutil).should_receive('rmtree').with_args(
'/mnt/subvol1/.borgmatic-1234'
).once()
@@ -846,3 +925,48 @@ def test_remove_data_source_dumps_with_delete_snapshot_called_process_error_bail
borgmatic_runtime_directory='/run/borgmatic',
dry_run=False,
)
def test_remove_data_source_dumps_with_root_subvolume_skips_duplicate_removal():
config = {'btrfs': {}}
flexmock(module).should_receive('get_subvolumes').and_return(
(module.Subvolume('/', contained_patterns=(Pattern('/etc'),)),)
)
flexmock(module).should_receive('make_snapshot_path').with_args('/').and_return(
'/.borgmatic-1234'
)
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).with_args(
'/.borgmatic-1234',
temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
).and_return(
'/.borgmatic-*'
)
flexmock(module.glob).should_receive('glob').with_args('/.borgmatic-*').and_return(
('/.borgmatic-1234', '/.borgmatic-5678')
)
flexmock(module.os.path).should_receive('isdir').with_args('/.borgmatic-1234').and_return(
True
).and_return(False)
flexmock(module.os.path).should_receive('isdir').with_args('/.borgmatic-5678').and_return(
True
).and_return(False)
flexmock(module).should_receive('delete_snapshot').with_args('btrfs', '/.borgmatic-1234').once()
flexmock(module).should_receive('delete_snapshot').with_args('btrfs', '/.borgmatic-5678').once()
flexmock(module.os.path).should_receive('isdir').with_args('').and_return(False)
flexmock(module.shutil).should_receive('rmtree').never()
module.remove_data_source_dumps(
hook_config=config['btrfs'],
config=config,
borgmatic_runtime_directory='/run/borgmatic',
dry_run=False,
)
+358 -137
View File
@@ -1,7 +1,7 @@
import pytest
from flexmock import flexmock
from borgmatic.borg.pattern import Pattern, Pattern_style, Pattern_type
from borgmatic.borg.pattern import Pattern, Pattern_source, Pattern_style, Pattern_type
from borgmatic.hooks.data_source import lvm as module
@@ -37,14 +37,20 @@ def test_get_logical_volumes_filters_by_patterns():
}
'''
)
contained = {Pattern('/mnt/lvolume'), Pattern('/mnt/lvolume/subdir')}
contained = {
Pattern('/mnt/lvolume', source=Pattern_source.CONFIG),
Pattern('/mnt/lvolume/subdir', source=Pattern_source.CONFIG),
}
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_patterns'
).with_args(None, contained).never()
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_patterns'
).with_args('/mnt/lvolume', contained).and_return(
(Pattern('/mnt/lvolume'), Pattern('/mnt/lvolume/subdir'))
(
Pattern('/mnt/lvolume', source=Pattern_source.CONFIG),
Pattern('/mnt/lvolume/subdir', source=Pattern_source.CONFIG),
)
)
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_patterns'
@@ -54,17 +60,116 @@ def test_get_logical_volumes_filters_by_patterns():
).with_args('/mnt/notlvm', contained).never()
assert module.get_logical_volumes(
'lsblk', patterns=(Pattern('/mnt/lvolume'), Pattern('/mnt/lvolume/subdir'))
'lsblk',
patterns=(
Pattern('/mnt/lvolume', source=Pattern_source.CONFIG),
Pattern('/mnt/lvolume/subdir', source=Pattern_source.CONFIG),
),
) == (
module.Logical_volume(
name='vgroup-lvolume',
device_path='/dev/mapper/vgroup-lvolume',
mount_point='/mnt/lvolume',
contained_patterns=(Pattern('/mnt/lvolume'), Pattern('/mnt/lvolume/subdir')),
contained_patterns=(
Pattern('/mnt/lvolume', source=Pattern_source.CONFIG),
Pattern('/mnt/lvolume/subdir', source=Pattern_source.CONFIG),
),
),
)
def test_get_logical_volumes_skips_non_root_patterns():
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).and_return(
'''
{
"blockdevices": [
{
"name": "vgroup-lvolume",
"path": "/dev/mapper/vgroup-lvolume",
"mountpoint": "/mnt/lvolume",
"type": "lvm"
}
]
}
'''
)
contained = {
Pattern('/mnt/lvolume', type=Pattern_type.EXCLUDE, source=Pattern_source.CONFIG),
Pattern('/mnt/lvolume/subdir', type=Pattern_type.EXCLUDE, source=Pattern_source.CONFIG),
}
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_patterns'
).with_args(None, contained).never()
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_patterns'
).with_args('/mnt/lvolume', contained).and_return(
(
Pattern('/mnt/lvolume', type=Pattern_type.EXCLUDE, source=Pattern_source.CONFIG),
Pattern('/mnt/lvolume/subdir', type=Pattern_type.EXCLUDE, source=Pattern_source.CONFIG),
)
)
assert (
module.get_logical_volumes(
'lsblk',
patterns=(
Pattern('/mnt/lvolume', type=Pattern_type.EXCLUDE, source=Pattern_source.CONFIG),
Pattern(
'/mnt/lvolume/subdir', type=Pattern_type.EXCLUDE, source=Pattern_source.CONFIG
),
),
)
== ()
)
def test_get_logical_volumes_skips_non_config_patterns():
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).and_return(
'''
{
"blockdevices": [
{
"name": "vgroup-lvolume",
"path": "/dev/mapper/vgroup-lvolume",
"mountpoint": "/mnt/lvolume",
"type": "lvm"
}
]
}
'''
)
contained = {
Pattern('/mnt/lvolume', source=Pattern_source.HOOK),
Pattern('/mnt/lvolume/subdir', source=Pattern_source.HOOK),
}
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_patterns'
).with_args(None, contained).never()
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_patterns'
).with_args('/mnt/lvolume', contained).and_return(
(
Pattern('/mnt/lvolume', source=Pattern_source.HOOK),
Pattern('/mnt/lvolume/subdir', source=Pattern_source.HOOK),
)
)
assert (
module.get_logical_volumes(
'lsblk',
patterns=(
Pattern('/mnt/lvolume', source=Pattern_source.HOOK),
Pattern('/mnt/lvolume/subdir', source=Pattern_source.HOOK),
),
)
== ()
)
def test_get_logical_volumes_with_invalid_lsblk_json_errors():
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
@@ -138,13 +243,13 @@ def test_snapshot_logical_volume_with_non_percentage_snapshot_name_uses_lvcreate
(
(
Pattern('/foo/bar/baz'),
Pattern('/run/borgmatic/lvm_snapshots/./foo/bar/baz'),
Pattern('/run/borgmatic/lvm_snapshots/b33f/./foo/bar/baz'),
),
(Pattern('/foo/bar'), Pattern('/run/borgmatic/lvm_snapshots/./foo/bar')),
(Pattern('/foo/bar'), Pattern('/run/borgmatic/lvm_snapshots/b33f/./foo/bar')),
(
Pattern('^/foo/bar', Pattern_type.INCLUDE, Pattern_style.REGULAR_EXPRESSION),
Pattern(
'^/run/borgmatic/lvm_snapshots/./foo/bar',
'^/run/borgmatic/lvm_snapshots/b33f/./foo/bar',
Pattern_type.INCLUDE,
Pattern_style.REGULAR_EXPRESSION,
),
@@ -152,40 +257,48 @@ def test_snapshot_logical_volume_with_non_percentage_snapshot_name_uses_lvcreate
(
Pattern('/foo/bar', Pattern_type.INCLUDE, Pattern_style.REGULAR_EXPRESSION),
Pattern(
'/run/borgmatic/lvm_snapshots/./foo/bar',
'/run/borgmatic/lvm_snapshots/b33f/./foo/bar',
Pattern_type.INCLUDE,
Pattern_style.REGULAR_EXPRESSION,
),
),
(Pattern('/foo'), Pattern('/run/borgmatic/lvm_snapshots/./foo')),
(Pattern('/'), Pattern('/run/borgmatic/lvm_snapshots/./')),
(Pattern('/foo'), Pattern('/run/borgmatic/lvm_snapshots/b33f/./foo')),
(Pattern('/'), Pattern('/run/borgmatic/lvm_snapshots/b33f/./')),
),
)
def test_make_borg_snapshot_pattern_includes_slashdot_hack_and_stripped_pattern_path(
pattern, expected_pattern
):
assert module.make_borg_snapshot_pattern(pattern, '/run/borgmatic') == expected_pattern
flexmock(module.hashlib).should_receive('shake_256').and_return(
flexmock(hexdigest=lambda length: 'b33f')
)
assert (
module.make_borg_snapshot_pattern(
pattern, flexmock(mount_point='/something'), '/run/borgmatic'
)
== expected_pattern
)
def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
config = {'lvm': {}}
patterns = [Pattern('/mnt/lvolume1/subdir'), Pattern('/mnt/lvolume2')]
flexmock(module).should_receive('get_logical_volumes').and_return(
(
module.Logical_volume(
name='lvolume1',
device_path='/dev/lvolume1',
mount_point='/mnt/lvolume1',
contained_patterns=(Pattern('/mnt/lvolume1/subdir'),),
),
module.Logical_volume(
name='lvolume2',
device_path='/dev/lvolume2',
mount_point='/mnt/lvolume2',
contained_patterns=(Pattern('/mnt/lvolume2'),),
),
)
logical_volumes = (
module.Logical_volume(
name='lvolume1',
device_path='/dev/lvolume1',
mount_point='/mnt/lvolume1',
contained_patterns=(Pattern('/mnt/lvolume1/subdir'),),
),
module.Logical_volume(
name='lvolume2',
device_path='/dev/lvolume2',
mount_point='/mnt/lvolume2',
contained_patterns=(Pattern('/mnt/lvolume2'),),
),
)
flexmock(module).should_receive('get_logical_volumes').and_return(logical_volumes)
flexmock(module.os).should_receive('getpid').and_return(1234)
flexmock(module).should_receive('snapshot_logical_volume').with_args(
'lvcreate', 'lvolume1_borgmatic-1234', '/dev/lvolume1', module.DEFAULT_SNAPSHOT_SIZE
@@ -203,18 +316,21 @@ def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
).and_return(
(module.Snapshot(name='lvolume2_borgmatic-1234', device_path='/dev/lvolume2_snap'),)
)
flexmock(module.hashlib).should_receive('shake_256').and_return(
flexmock(hexdigest=lambda length: 'b33f')
)
flexmock(module).should_receive('mount_snapshot').with_args(
'mount', '/dev/lvolume1_snap', '/run/borgmatic/lvm_snapshots/mnt/lvolume1'
'mount', '/dev/lvolume1_snap', '/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume1'
).once()
flexmock(module).should_receive('mount_snapshot').with_args(
'mount', '/dev/lvolume2_snap', '/run/borgmatic/lvm_snapshots/mnt/lvolume2'
'mount', '/dev/lvolume2_snap', '/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume2'
).once()
flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
Pattern('/mnt/lvolume1/subdir'), '/run/borgmatic'
).and_return(Pattern('/run/borgmatic/lvm_snapshots/./mnt/lvolume1/subdir'))
Pattern('/mnt/lvolume1/subdir'), logical_volumes[0], '/run/borgmatic'
).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir'))
flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
Pattern('/mnt/lvolume2'), '/run/borgmatic'
).and_return(Pattern('/run/borgmatic/lvm_snapshots/./mnt/lvolume2'))
Pattern('/mnt/lvolume2'), logical_volumes[1], '/run/borgmatic'
).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'))
assert (
module.dump_data_sources(
@@ -229,8 +345,8 @@ def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
)
assert patterns == [
Pattern('/run/borgmatic/lvm_snapshots/./mnt/lvolume1/subdir'),
Pattern('/run/borgmatic/lvm_snapshots/./mnt/lvolume2'),
Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir'),
Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'),
]
@@ -259,22 +375,21 @@ def test_dump_data_sources_with_no_logical_volumes_skips_snapshots():
def test_dump_data_sources_uses_snapshot_size_for_snapshot():
config = {'lvm': {'snapshot_size': '1000PB'}}
patterns = [Pattern('/mnt/lvolume1/subdir'), Pattern('/mnt/lvolume2')]
flexmock(module).should_receive('get_logical_volumes').and_return(
(
module.Logical_volume(
name='lvolume1',
device_path='/dev/lvolume1',
mount_point='/mnt/lvolume1',
contained_patterns=(Pattern('/mnt/lvolume1/subdir'),),
),
module.Logical_volume(
name='lvolume2',
device_path='/dev/lvolume2',
mount_point='/mnt/lvolume2',
contained_patterns=(Pattern('/mnt/lvolume2'),),
),
)
logical_volumes = (
module.Logical_volume(
name='lvolume1',
device_path='/dev/lvolume1',
mount_point='/mnt/lvolume1',
contained_patterns=(Pattern('/mnt/lvolume1/subdir'),),
),
module.Logical_volume(
name='lvolume2',
device_path='/dev/lvolume2',
mount_point='/mnt/lvolume2',
contained_patterns=(Pattern('/mnt/lvolume2'),),
),
)
flexmock(module).should_receive('get_logical_volumes').and_return(logical_volumes)
flexmock(module.os).should_receive('getpid').and_return(1234)
flexmock(module).should_receive('snapshot_logical_volume').with_args(
'lvcreate',
@@ -298,18 +413,21 @@ def test_dump_data_sources_uses_snapshot_size_for_snapshot():
).and_return(
(module.Snapshot(name='lvolume2_borgmatic-1234', device_path='/dev/lvolume2_snap'),)
)
flexmock(module.hashlib).should_receive('shake_256').and_return(
flexmock(hexdigest=lambda length: 'b33f')
)
flexmock(module).should_receive('mount_snapshot').with_args(
'mount', '/dev/lvolume1_snap', '/run/borgmatic/lvm_snapshots/mnt/lvolume1'
'mount', '/dev/lvolume1_snap', '/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume1'
).once()
flexmock(module).should_receive('mount_snapshot').with_args(
'mount', '/dev/lvolume2_snap', '/run/borgmatic/lvm_snapshots/mnt/lvolume2'
'mount', '/dev/lvolume2_snap', '/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume2'
).once()
flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
Pattern('/mnt/lvolume1/subdir'), '/run/borgmatic'
).and_return(Pattern('/run/borgmatic/lvm_snapshots/./mnt/lvolume1/subdir'))
Pattern('/mnt/lvolume1/subdir'), logical_volumes[0], '/run/borgmatic'
).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir'))
flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
Pattern('/mnt/lvolume2'), '/run/borgmatic'
).and_return(Pattern('/run/borgmatic/lvm_snapshots/./mnt/lvolume2'))
Pattern('/mnt/lvolume2'), logical_volumes[1], '/run/borgmatic'
).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'))
assert (
module.dump_data_sources(
@@ -324,8 +442,8 @@ def test_dump_data_sources_uses_snapshot_size_for_snapshot():
)
assert patterns == [
Pattern('/run/borgmatic/lvm_snapshots/./mnt/lvolume1/subdir'),
Pattern('/run/borgmatic/lvm_snapshots/./mnt/lvolume2'),
Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir'),
Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'),
]
@@ -339,22 +457,21 @@ def test_dump_data_sources_uses_custom_commands():
},
}
patterns = [Pattern('/mnt/lvolume1/subdir'), Pattern('/mnt/lvolume2')]
flexmock(module).should_receive('get_logical_volumes').and_return(
(
module.Logical_volume(
name='lvolume1',
device_path='/dev/lvolume1',
mount_point='/mnt/lvolume1',
contained_patterns=(Pattern('/mnt/lvolume1/subdir'),),
),
module.Logical_volume(
name='lvolume2',
device_path='/dev/lvolume2',
mount_point='/mnt/lvolume2',
contained_patterns=(Pattern('/mnt/lvolume2'),),
),
)
logical_volumes = (
module.Logical_volume(
name='lvolume1',
device_path='/dev/lvolume1',
mount_point='/mnt/lvolume1',
contained_patterns=(Pattern('/mnt/lvolume1/subdir'),),
),
module.Logical_volume(
name='lvolume2',
device_path='/dev/lvolume2',
mount_point='/mnt/lvolume2',
contained_patterns=(Pattern('/mnt/lvolume2'),),
),
)
flexmock(module).should_receive('get_logical_volumes').and_return(logical_volumes)
flexmock(module.os).should_receive('getpid').and_return(1234)
flexmock(module).should_receive('snapshot_logical_volume').with_args(
'/usr/local/bin/lvcreate',
@@ -378,18 +495,25 @@ def test_dump_data_sources_uses_custom_commands():
).and_return(
(module.Snapshot(name='lvolume2_borgmatic-1234', device_path='/dev/lvolume2_snap'),)
)
flexmock(module.hashlib).should_receive('shake_256').and_return(
flexmock(hexdigest=lambda length: 'b33f')
)
flexmock(module).should_receive('mount_snapshot').with_args(
'/usr/local/bin/mount', '/dev/lvolume1_snap', '/run/borgmatic/lvm_snapshots/mnt/lvolume1'
'/usr/local/bin/mount',
'/dev/lvolume1_snap',
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume1',
).once()
flexmock(module).should_receive('mount_snapshot').with_args(
'/usr/local/bin/mount', '/dev/lvolume2_snap', '/run/borgmatic/lvm_snapshots/mnt/lvolume2'
'/usr/local/bin/mount',
'/dev/lvolume2_snap',
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume2',
).once()
flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
Pattern('/mnt/lvolume1/subdir'), '/run/borgmatic'
).and_return(Pattern('/run/borgmatic/lvm_snapshots/./mnt/lvolume1/subdir'))
Pattern('/mnt/lvolume1/subdir'), logical_volumes[0], '/run/borgmatic'
).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir'))
flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
Pattern('/mnt/lvolume2'), '/run/borgmatic'
).and_return(Pattern('/run/borgmatic/lvm_snapshots/./mnt/lvolume2'))
Pattern('/mnt/lvolume2'), logical_volumes[1], '/run/borgmatic'
).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'))
assert (
module.dump_data_sources(
@@ -404,8 +528,8 @@ def test_dump_data_sources_uses_custom_commands():
)
assert patterns == [
Pattern('/run/borgmatic/lvm_snapshots/./mnt/lvolume1/subdir'),
Pattern('/run/borgmatic/lvm_snapshots/./mnt/lvolume2'),
Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir'),
Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'),
]
@@ -463,22 +587,21 @@ def test_dump_data_sources_with_dry_run_skips_snapshots_and_does_not_touch_patte
def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained_patterns():
config = {'lvm': {}}
patterns = [Pattern('/hmm')]
flexmock(module).should_receive('get_logical_volumes').and_return(
(
module.Logical_volume(
name='lvolume1',
device_path='/dev/lvolume1',
mount_point='/mnt/lvolume1',
contained_patterns=(Pattern('/mnt/lvolume1/subdir'),),
),
module.Logical_volume(
name='lvolume2',
device_path='/dev/lvolume2',
mount_point='/mnt/lvolume2',
contained_patterns=(Pattern('/mnt/lvolume2'),),
),
)
logical_volumes = (
module.Logical_volume(
name='lvolume1',
device_path='/dev/lvolume1',
mount_point='/mnt/lvolume1',
contained_patterns=(Pattern('/mnt/lvolume1/subdir'),),
),
module.Logical_volume(
name='lvolume2',
device_path='/dev/lvolume2',
mount_point='/mnt/lvolume2',
contained_patterns=(Pattern('/mnt/lvolume2'),),
),
)
flexmock(module).should_receive('get_logical_volumes').and_return(logical_volumes)
flexmock(module.os).should_receive('getpid').and_return(1234)
flexmock(module).should_receive('snapshot_logical_volume').with_args(
'lvcreate', 'lvolume1_borgmatic-1234', '/dev/lvolume1', module.DEFAULT_SNAPSHOT_SIZE
@@ -496,18 +619,21 @@ def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained
).and_return(
(module.Snapshot(name='lvolume2_borgmatic-1234', device_path='/dev/lvolume2_snap'),)
)
flexmock(module.hashlib).should_receive('shake_256').and_return(
flexmock(hexdigest=lambda length: 'b33f')
)
flexmock(module).should_receive('mount_snapshot').with_args(
'mount', '/dev/lvolume1_snap', '/run/borgmatic/lvm_snapshots/mnt/lvolume1'
'mount', '/dev/lvolume1_snap', '/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume1'
).once()
flexmock(module).should_receive('mount_snapshot').with_args(
'mount', '/dev/lvolume2_snap', '/run/borgmatic/lvm_snapshots/mnt/lvolume2'
'mount', '/dev/lvolume2_snap', '/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume2'
).once()
flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
Pattern('/mnt/lvolume1/subdir'), '/run/borgmatic'
).and_return(Pattern('/run/borgmatic/lvm_snapshots/./mnt/lvolume1/subdir'))
Pattern('/mnt/lvolume1/subdir'), logical_volumes[0], '/run/borgmatic'
).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir'))
flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
Pattern('/mnt/lvolume2'), '/run/borgmatic'
).and_return(Pattern('/run/borgmatic/lvm_snapshots/./mnt/lvolume2'))
Pattern('/mnt/lvolume2'), logical_volumes[1], '/run/borgmatic'
).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'))
assert (
module.dump_data_sources(
@@ -523,8 +649,8 @@ def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained
assert patterns == [
Pattern('/hmm'),
Pattern('/run/borgmatic/lvm_snapshots/./mnt/lvolume1/subdir'),
Pattern('/run/borgmatic/lvm_snapshots/./mnt/lvolume2'),
Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir'),
Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'),
]
@@ -694,16 +820,19 @@ def test_remove_data_source_dumps_unmounts_and_remove_snapshots():
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).and_return('/run/borgmatic')
flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
flexmock(module.glob).should_receive('glob').replace_with(
lambda path: [path.replace('*', 'b33f')]
)
flexmock(module.os.path).should_receive('isdir').and_return(True)
flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
flexmock(module.shutil).should_receive('rmtree')
flexmock(module).should_receive('unmount_snapshot').with_args(
'umount',
'/run/borgmatic/lvm_snapshots/mnt/lvolume1',
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume1',
).once()
flexmock(module).should_receive('unmount_snapshot').with_args(
'umount',
'/run/borgmatic/lvm_snapshots/mnt/lvolume2',
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume2',
).once()
flexmock(module).should_receive('get_snapshots').and_return(
(
@@ -799,9 +928,11 @@ def test_remove_data_source_dumps_with_missing_snapshot_directory_skips_unmount(
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).and_return('/run/borgmatic')
flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
flexmock(module.glob).should_receive('glob').replace_with(
lambda path: [path.replace('*', 'b33f')]
)
flexmock(module.os.path).should_receive('isdir').with_args(
'/run/borgmatic/lvm_snapshots'
'/run/borgmatic/lvm_snapshots/b33f'
).and_return(False)
flexmock(module.shutil).should_receive('rmtree').never()
flexmock(module).should_receive('unmount_snapshot').never()
@@ -843,24 +974,92 @@ def test_remove_data_source_dumps_with_missing_snapshot_mount_path_skips_unmount
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).and_return('/run/borgmatic')
flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
flexmock(module.glob).should_receive('glob').replace_with(
lambda path: [path.replace('*', 'b33f')]
)
flexmock(module.os.path).should_receive('isdir').with_args(
'/run/borgmatic/lvm_snapshots'
'/run/borgmatic/lvm_snapshots/b33f'
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/run/borgmatic/lvm_snapshots/mnt/lvolume1'
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume1'
).and_return(False)
flexmock(module.os.path).should_receive('isdir').with_args(
'/run/borgmatic/lvm_snapshots/mnt/lvolume2'
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume2'
).and_return(True)
flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
flexmock(module.shutil).should_receive('rmtree')
flexmock(module).should_receive('unmount_snapshot').with_args(
'umount',
'/run/borgmatic/lvm_snapshots/mnt/lvolume1',
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume1',
).never()
flexmock(module).should_receive('unmount_snapshot').with_args(
'umount',
'/run/borgmatic/lvm_snapshots/mnt/lvolume2',
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume2',
).once()
flexmock(module).should_receive('get_snapshots').and_return(
(
module.Snapshot('lvolume1_borgmatic-1234', '/dev/lvolume1'),
module.Snapshot('lvolume2_borgmatic-1234', '/dev/lvolume2'),
),
)
flexmock(module).should_receive('remove_snapshot').with_args('lvremove', '/dev/lvolume1').once()
flexmock(module).should_receive('remove_snapshot').with_args('lvremove', '/dev/lvolume2').once()
module.remove_data_source_dumps(
hook_config=config['lvm'],
config=config,
borgmatic_runtime_directory='/run/borgmatic',
dry_run=False,
)
def test_remove_data_source_dumps_with_empty_snapshot_mount_path_skips_unmount():
config = {'lvm': {}}
flexmock(module).should_receive('get_logical_volumes').and_return(
(
module.Logical_volume(
name='lvolume1',
device_path='/dev/lvolume1',
mount_point='/mnt/lvolume1',
contained_patterns=(Pattern('/mnt/lvolume1/subdir'),),
),
module.Logical_volume(
name='lvolume2',
device_path='/dev/lvolume2',
mount_point='/mnt/lvolume2',
contained_patterns=(Pattern('/mnt/lvolume2'),),
),
)
)
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).and_return('/run/borgmatic')
flexmock(module.glob).should_receive('glob').replace_with(
lambda path: [path.replace('*', 'b33f')]
)
flexmock(module.os.path).should_receive('isdir').with_args(
'/run/borgmatic/lvm_snapshots/b33f'
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume1'
).and_return(True)
flexmock(module.os).should_receive('listdir').with_args(
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume1'
).and_return([])
flexmock(module.os.path).should_receive('isdir').with_args(
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume2'
).and_return(True)
flexmock(module.os).should_receive('listdir').with_args(
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume2'
).and_return(['file.txt'])
flexmock(module.shutil).should_receive('rmtree')
flexmock(module).should_receive('unmount_snapshot').with_args(
'umount',
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume1',
).never()
flexmock(module).should_receive('unmount_snapshot').with_args(
'umount',
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume2',
).once()
flexmock(module).should_receive('get_snapshots').and_return(
(
@@ -900,24 +1099,27 @@ def test_remove_data_source_dumps_with_successful_mount_point_removal_skips_unmo
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).and_return('/run/borgmatic')
flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
flexmock(module.glob).should_receive('glob').replace_with(
lambda path: [path.replace('*', 'b33f')]
)
flexmock(module.os.path).should_receive('isdir').with_args(
'/run/borgmatic/lvm_snapshots'
'/run/borgmatic/lvm_snapshots/b33f'
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/run/borgmatic/lvm_snapshots/mnt/lvolume1'
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume1'
).and_return(True).and_return(False)
flexmock(module.os.path).should_receive('isdir').with_args(
'/run/borgmatic/lvm_snapshots/mnt/lvolume2'
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume2'
).and_return(True).and_return(True)
flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
flexmock(module.shutil).should_receive('rmtree')
flexmock(module).should_receive('unmount_snapshot').with_args(
'umount',
'/run/borgmatic/lvm_snapshots/mnt/lvolume1',
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume1',
).never()
flexmock(module).should_receive('unmount_snapshot').with_args(
'umount',
'/run/borgmatic/lvm_snapshots/mnt/lvolume2',
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume2',
).once()
flexmock(module).should_receive('get_snapshots').and_return(
(
@@ -957,16 +1159,19 @@ def test_remove_data_source_dumps_bails_for_missing_umount_command():
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).and_return('/run/borgmatic')
flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
flexmock(module.glob).should_receive('glob').replace_with(
lambda path: [path.replace('*', 'b33f')]
)
flexmock(module.os.path).should_receive('isdir').and_return(True)
flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
flexmock(module.shutil).should_receive('rmtree')
flexmock(module).should_receive('unmount_snapshot').with_args(
'umount',
'/run/borgmatic/lvm_snapshots/mnt/lvolume1',
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume1',
).and_raise(FileNotFoundError)
flexmock(module).should_receive('unmount_snapshot').with_args(
'umount',
'/run/borgmatic/lvm_snapshots/mnt/lvolume2',
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume2',
).never()
flexmock(module).should_receive('get_snapshots').never()
flexmock(module).should_receive('remove_snapshot').never()
@@ -979,7 +1184,7 @@ def test_remove_data_source_dumps_bails_for_missing_umount_command():
)
def test_remove_data_source_dumps_bails_for_umount_command_error():
def test_remove_data_source_dumps_swallows_umount_command_error():
config = {'lvm': {}}
flexmock(module).should_receive('get_logical_volumes').and_return(
(
@@ -1000,19 +1205,28 @@ def test_remove_data_source_dumps_bails_for_umount_command_error():
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).and_return('/run/borgmatic')
flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
flexmock(module.glob).should_receive('glob').replace_with(
lambda path: [path.replace('*', 'b33f')]
)
flexmock(module.os.path).should_receive('isdir').and_return(True)
flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
flexmock(module.shutil).should_receive('rmtree')
flexmock(module).should_receive('unmount_snapshot').with_args(
'umount',
'/run/borgmatic/lvm_snapshots/mnt/lvolume1',
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume1',
).and_raise(module.subprocess.CalledProcessError(1, 'wtf'))
flexmock(module).should_receive('unmount_snapshot').with_args(
'umount',
'/run/borgmatic/lvm_snapshots/mnt/lvolume2',
).never()
flexmock(module).should_receive('get_snapshots').never()
flexmock(module).should_receive('remove_snapshot').never()
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume2',
).once()
flexmock(module).should_receive('get_snapshots').and_return(
(
module.Snapshot('lvolume1_borgmatic-1234', '/dev/lvolume1'),
module.Snapshot('lvolume2_borgmatic-1234', '/dev/lvolume2'),
),
)
flexmock(module).should_receive('remove_snapshot').with_args('lvremove', '/dev/lvolume1').once()
flexmock(module).should_receive('remove_snapshot').with_args('lvremove', '/dev/lvolume2').once()
module.remove_data_source_dumps(
hook_config=config['lvm'],
@@ -1043,16 +1257,19 @@ def test_remove_data_source_dumps_bails_for_missing_lvs_command():
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).and_return('/run/borgmatic')
flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
flexmock(module.glob).should_receive('glob').replace_with(
lambda path: [path.replace('*', 'b33f')]
)
flexmock(module.os.path).should_receive('isdir').and_return(True)
flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
flexmock(module.shutil).should_receive('rmtree')
flexmock(module).should_receive('unmount_snapshot').with_args(
'umount',
'/run/borgmatic/lvm_snapshots/mnt/lvolume1',
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume1',
).once()
flexmock(module).should_receive('unmount_snapshot').with_args(
'umount',
'/run/borgmatic/lvm_snapshots/mnt/lvolume2',
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume2',
).once()
flexmock(module).should_receive('get_snapshots').and_raise(FileNotFoundError)
flexmock(module).should_receive('remove_snapshot').never()
@@ -1086,16 +1303,19 @@ def test_remove_data_source_dumps_bails_for_lvs_command_error():
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).and_return('/run/borgmatic')
flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
flexmock(module.glob).should_receive('glob').replace_with(
lambda path: [path.replace('*', 'b33f')]
)
flexmock(module.os.path).should_receive('isdir').and_return(True)
flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
flexmock(module.shutil).should_receive('rmtree')
flexmock(module).should_receive('unmount_snapshot').with_args(
'umount',
'/run/borgmatic/lvm_snapshots/mnt/lvolume1',
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume1',
).once()
flexmock(module).should_receive('unmount_snapshot').with_args(
'umount',
'/run/borgmatic/lvm_snapshots/mnt/lvolume2',
'/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume2',
).once()
flexmock(module).should_receive('get_snapshots').and_raise(
module.subprocess.CalledProcessError(1, 'wtf')
@@ -1133,6 +1353,7 @@ def test_remove_data_source_with_dry_run_skips_snapshot_unmount_and_delete():
).and_return('/run/borgmatic')
flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
flexmock(module.os.path).should_receive('isdir').and_return(True)
flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
flexmock(module.shutil).should_receive('rmtree').never()
flexmock(module).should_receive('unmount_snapshot').never()
flexmock(module).should_receive('get_snapshots').and_return(
+84 -58
View File
@@ -7,33 +7,33 @@ from borgmatic.hooks.data_source import mariadb as module
def test_database_names_to_dump_passes_through_name():
extra_environment = flexmock()
environment = flexmock()
names = module.database_names_to_dump({'name': 'foo'}, extra_environment, dry_run=False)
names = module.database_names_to_dump({'name': 'foo'}, {}, environment, dry_run=False)
assert names == ('foo',)
def test_database_names_to_dump_bails_for_dry_run():
extra_environment = flexmock()
environment = flexmock()
flexmock(module).should_receive('execute_command_and_capture_output').never()
names = module.database_names_to_dump({'name': 'all'}, extra_environment, dry_run=True)
names = module.database_names_to_dump({'name': 'all'}, {}, environment, dry_run=True)
assert names == ()
def test_database_names_to_dump_queries_mariadb_for_database_names():
extra_environment = flexmock()
environment = flexmock()
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('mariadb', '--skip-column-names', '--batch', '--execute', 'show schemas'),
extra_environment=extra_environment,
environment=environment,
).and_return('foo\nbar\nmysql\n').once()
names = module.database_names_to_dump({'name': 'all'}, extra_environment, dry_run=False)
names = module.database_names_to_dump({'name': 'all'}, {}, environment, dry_run=False)
assert names == ('foo', 'bar')
@@ -53,9 +53,10 @@ 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.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
('bar',)
)
@@ -63,9 +64,10 @@ def test_dump_data_sources_dumps_each_database():
for name, process in zip(('foo', 'bar'), processes):
flexmock(module).should_receive('execute_dump_command').with_args(
database={'name': name},
config={},
dump_path=object,
database_names=(name,),
extra_environment=object,
environment={'USER': 'root'},
dry_run=object,
dry_run_label=object,
).and_return(process).once()
@@ -87,18 +89,20 @@ 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: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
('bar',)
)
flexmock(module).should_receive('execute_dump_command').with_args(
database=database,
config={},
dump_path=object,
database_names=('foo',),
extra_environment={'MYSQL_PWD': 'trustsome1'},
environment={'USER': 'root', 'MYSQL_PWD': 'trustsome1'},
dry_run=object,
dry_run_label=object,
).and_return(process).once()
@@ -117,15 +121,17 @@ 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: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('database_names_to_dump').and_return(('foo', 'bar'))
flexmock(module).should_receive('execute_dump_command').with_args(
database={'name': 'all'},
config={},
dump_path=object,
database_names=('foo', 'bar'),
extra_environment=object,
environment={'USER': 'root'},
dry_run=object,
dry_run_label=object,
).and_return(process).once()
@@ -144,17 +150,19 @@ 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: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('database_names_to_dump').and_return(('foo', 'bar'))
for name, process in zip(('foo', 'bar'), processes):
flexmock(module).should_receive('execute_dump_command').with_args(
database={'name': name, 'format': 'sql'},
config={},
dump_path=object,
database_names=(name,),
extra_environment=object,
environment={'USER': 'root'},
dry_run=object,
dry_run_label=object,
).and_return(process).once()
@@ -183,10 +191,10 @@ def test_database_names_to_dump_runs_mariadb_with_list_options():
'--execute',
'show schemas',
),
extra_environment=None,
environment=None,
).and_return(('foo\nbar')).once()
assert module.database_names_to_dump(database, None, '') == ('foo', 'bar')
assert module.database_names_to_dump(database, {}, None, '') == ('foo', 'bar')
def test_database_names_to_dump_runs_non_default_mariadb_with_list_options():
@@ -196,7 +204,7 @@ def test_database_names_to_dump_runs_non_default_mariadb_with_list_options():
'mariadb_command': 'custom_mariadb',
}
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
extra_environment=None,
environment=None,
full_command=(
'custom_mariadb', # Custom MariaDB command
'--defaults-extra-file=mariadb.cnf',
@@ -207,7 +215,7 @@ def test_database_names_to_dump_runs_non_default_mariadb_with_list_options():
),
).and_return(('foo\nbar')).once()
assert module.database_names_to_dump(database, None, '') == ('foo', 'bar')
assert module.database_names_to_dump(database, {}, None, '') == ('foo', 'bar')
def test_execute_dump_command_runs_mariadb_dump():
@@ -216,7 +224,7 @@ def test_execute_dump_command_runs_mariadb_dump():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -228,16 +236,17 @@ def test_execute_dump_command_runs_mariadb_dump():
'--result-file',
'dump',
),
extra_environment=None,
environment=None,
run_to_completion=False,
).and_return(process).once()
assert (
module.execute_dump_command(
database={'name': 'foo'},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
environment=None,
dry_run=False,
dry_run_label='',
)
@@ -251,7 +260,7 @@ def test_execute_dump_command_runs_mariadb_dump_without_add_drop_database():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -262,16 +271,17 @@ def test_execute_dump_command_runs_mariadb_dump_without_add_drop_database():
'--result-file',
'dump',
),
extra_environment=None,
environment=None,
run_to_completion=False,
).and_return(process).once()
assert (
module.execute_dump_command(
database={'name': 'foo', 'add_drop_database': False},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
environment=None,
dry_run=False,
dry_run_label='',
)
@@ -285,7 +295,7 @@ def test_execute_dump_command_runs_mariadb_dump_with_hostname_and_port():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -303,16 +313,17 @@ def test_execute_dump_command_runs_mariadb_dump_with_hostname_and_port():
'--result-file',
'dump',
),
extra_environment=None,
environment=None,
run_to_completion=False,
).and_return(process).once()
assert (
module.execute_dump_command(
database={'name': 'foo', 'hostname': 'database.example.org', 'port': 5433},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
environment=None,
dry_run=False,
dry_run_label='',
)
@@ -326,7 +337,7 @@ def test_execute_dump_command_runs_mariadb_dump_with_username_and_password():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -340,16 +351,17 @@ def test_execute_dump_command_runs_mariadb_dump_with_username_and_password():
'--result-file',
'dump',
),
extra_environment={'MYSQL_PWD': 'trustsome1'},
environment={'MYSQL_PWD': 'trustsome1'},
run_to_completion=False,
).and_return(process).once()
assert (
module.execute_dump_command(
database={'name': 'foo', 'username': 'root', 'password': 'trustsome1'},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment={'MYSQL_PWD': 'trustsome1'},
environment={'MYSQL_PWD': 'trustsome1'},
dry_run=False,
dry_run_label='',
)
@@ -363,7 +375,7 @@ def test_execute_dump_command_runs_mariadb_dump_with_options():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -376,16 +388,17 @@ def test_execute_dump_command_runs_mariadb_dump_with_options():
'--result-file',
'dump',
),
extra_environment=None,
environment=None,
run_to_completion=False,
).and_return(process).once()
assert (
module.execute_dump_command(
database={'name': 'foo', 'options': '--stuff=such'},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
environment=None,
dry_run=False,
dry_run_label='',
)
@@ -399,7 +412,7 @@ def test_execute_dump_command_runs_non_default_mariadb_dump_with_options():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -412,7 +425,7 @@ def test_execute_dump_command_runs_non_default_mariadb_dump_with_options():
'--result-file',
'dump',
),
extra_environment=None,
environment=None,
run_to_completion=False,
).and_return(process).once()
@@ -423,9 +436,10 @@ def test_execute_dump_command_runs_non_default_mariadb_dump_with_options():
'mariadb_dump_command': 'custom_mariadb_dump',
'options': '--stuff=such',
}, # Custom MariaDB dump command specified
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
environment=None,
dry_run=False,
dry_run_label='',
)
@@ -442,9 +456,10 @@ def test_execute_dump_command_with_duplicate_dump_skips_mariadb_dump():
assert (
module.execute_dump_command(
database={'name': 'foo'},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
environment=None,
dry_run=True,
dry_run_label='SO DRY',
)
@@ -457,7 +472,7 @@ def test_execute_dump_command_with_dry_run_skips_mariadb_dump():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').never()
@@ -465,9 +480,10 @@ def test_execute_dump_command_with_dry_run_skips_mariadb_dump():
assert (
module.execute_dump_command(
database={'name': 'foo'},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
environment=None,
dry_run=True,
dry_run_label='SO DRY',
)
@@ -478,9 +494,10 @@ def test_execute_dump_command_with_dry_run_skips_mariadb_dump():
def test_dump_data_sources_errors_for_missing_all_databases():
databases = [{'name': 'all'}]
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
'databases/localhost/all'
)
@@ -500,9 +517,10 @@ def test_dump_data_sources_errors_for_missing_all_databases():
def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run():
databases = [{'name': 'all'}]
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
'databases/localhost/all'
)
@@ -527,13 +545,14 @@ def test_restore_data_source_dump_runs_mariadb_to_restore():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
('mariadb', '--batch'),
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment=None,
environment={'USER': 'root'},
).once()
module.restore_data_source_dump(
@@ -558,13 +577,14 @@ def test_restore_data_source_dump_runs_mariadb_with_options():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
('mariadb', '--batch', '--harder'),
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment=None,
environment={'USER': 'root'},
).once()
module.restore_data_source_dump(
@@ -591,13 +611,14 @@ def test_restore_data_source_dump_runs_non_default_mariadb_with_options():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
('custom_mariadb', '--batch', '--harder'),
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment=None,
environment={'USER': 'root'},
).once()
module.restore_data_source_dump(
@@ -622,7 +643,8 @@ def test_restore_data_source_dump_runs_mariadb_with_hostname_and_port():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mariadb',
@@ -637,7 +659,7 @@ def test_restore_data_source_dump_runs_mariadb_with_hostname_and_port():
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment=None,
environment={'USER': 'root'},
).once()
module.restore_data_source_dump(
@@ -662,13 +684,14 @@ def test_restore_data_source_dump_runs_mariadb_with_username_and_password():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
('mariadb', '--batch', '--user', 'root'),
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment={'MYSQL_PWD': 'trustsome1'},
environment={'USER': 'root', 'MYSQL_PWD': 'trustsome1'},
).once()
module.restore_data_source_dump(
@@ -703,7 +726,8 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mariadb',
@@ -720,7 +744,7 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment={'MYSQL_PWD': 'clipassword'},
environment={'USER': 'root', 'MYSQL_PWD': 'clipassword'},
).once()
module.restore_data_source_dump(
@@ -757,7 +781,8 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mariadb',
@@ -774,7 +799,7 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment={'MYSQL_PWD': 'restorepass'},
environment={'USER': 'root', 'MYSQL_PWD': 'restorepass'},
).once()
module.restore_data_source_dump(
@@ -798,7 +823,8 @@ def test_restore_data_source_dump_with_dry_run_skips_restore():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').never()
module.restore_data_source_dump(
+13 -13
View File
@@ -126,7 +126,7 @@ def test_dump_data_sources_runs_mongodump_with_username_and_password():
)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -247,9 +247,9 @@ def test_build_dump_command_with_username_injection_attack_gets_escaped():
database = {'name': 'test', 'username': 'bob; naughty-command'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
command = module.build_dump_command(database, dump_filename='test', dump_format='archive')
command = module.build_dump_command(database, {}, dump_filename='test', dump_format='archive')
assert "'bob; naughty-command'" in command
@@ -262,7 +262,7 @@ def test_restore_data_source_dump_runs_mongorestore():
flexmock(module.dump).should_receive('make_data_source_dump_filename')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
['mongorestore', '--archive', '--drop'],
processes=[extract_process],
@@ -296,7 +296,7 @@ def test_restore_data_source_dump_runs_mongorestore_with_hostname_and_port():
flexmock(module.dump).should_receive('make_data_source_dump_filename')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
[
'mongorestore',
@@ -344,7 +344,7 @@ def test_restore_data_source_dump_runs_mongorestore_with_username_and_password()
flexmock(module.dump).should_receive('make_data_source_dump_filename')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
[
'mongorestore',
@@ -398,7 +398,7 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
flexmock(module.dump).should_receive('make_data_source_dump_filename')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
[
'mongorestore',
@@ -456,7 +456,7 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
flexmock(module.dump).should_receive('make_data_source_dump_filename')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
[
'mongorestore',
@@ -502,7 +502,7 @@ def test_restore_data_source_dump_runs_mongorestore_with_options():
flexmock(module.dump).should_receive('make_data_source_dump_filename')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
['mongorestore', '--archive', '--drop', '--harder'],
processes=[extract_process],
@@ -534,7 +534,7 @@ def test_restore_databases_dump_runs_mongorestore_with_schemas():
flexmock(module.dump).should_receive('make_data_source_dump_filename')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
[
'mongorestore',
@@ -574,7 +574,7 @@ def test_restore_data_source_dump_runs_psql_for_all_database_dump():
flexmock(module.dump).should_receive('make_data_source_dump_filename')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
['mongorestore', '--archive'],
processes=[extract_process],
@@ -605,7 +605,7 @@ def test_restore_data_source_dump_with_dry_run_skips_restore():
flexmock(module.dump).should_receive('make_data_source_dump_filename')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').never()
module.restore_data_source_dump(
@@ -631,7 +631,7 @@ def test_restore_data_source_dump_without_extract_process_restores_from_disk():
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('/dump/path')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
['mongorestore', '--dir', '/dump/path', '--drop'],
processes=[],
+82 -56
View File
@@ -7,36 +7,36 @@ from borgmatic.hooks.data_source import mysql as module
def test_database_names_to_dump_passes_through_name():
extra_environment = flexmock()
environment = flexmock()
names = module.database_names_to_dump({'name': 'foo'}, extra_environment, dry_run=False)
names = module.database_names_to_dump({'name': 'foo'}, {}, environment, dry_run=False)
assert names == ('foo',)
def test_database_names_to_dump_bails_for_dry_run():
extra_environment = flexmock()
environment = flexmock()
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_and_capture_output').never()
names = module.database_names_to_dump({'name': 'all'}, extra_environment, dry_run=True)
names = module.database_names_to_dump({'name': 'all'}, {}, environment, dry_run=True)
assert names == ()
def test_database_names_to_dump_queries_mysql_for_database_names():
extra_environment = flexmock()
environment = flexmock()
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('mysql', '--skip-column-names', '--batch', '--execute', 'show schemas'),
extra_environment=extra_environment,
environment=environment,
).and_return('foo\nbar\nmysql\n').once()
names = module.database_names_to_dump({'name': 'all'}, extra_environment, dry_run=False)
names = module.database_names_to_dump({'name': 'all'}, {}, environment, dry_run=False)
assert names == ('foo', 'bar')
@@ -56,6 +56,7 @@ 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.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
('bar',)
)
@@ -63,9 +64,10 @@ def test_dump_data_sources_dumps_each_database():
for name, process in zip(('foo', 'bar'), processes):
flexmock(module).should_receive('execute_dump_command').with_args(
database={'name': name},
config={},
dump_path=object,
database_names=(name,),
extra_environment=object,
environment={'USER': 'root'},
dry_run=object,
dry_run_label=object,
).and_return(process).once()
@@ -87,18 +89,20 @@ 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: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
('bar',)
)
flexmock(module).should_receive('execute_dump_command').with_args(
database=database,
config={},
dump_path=object,
database_names=('foo',),
extra_environment={'MYSQL_PWD': 'trustsome1'},
environment={'USER': 'root', 'MYSQL_PWD': 'trustsome1'},
dry_run=object,
dry_run_label=object,
).and_return(process).once()
@@ -117,12 +121,14 @@ 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).should_receive('database_names_to_dump').and_return(('foo', 'bar'))
flexmock(module).should_receive('execute_dump_command').with_args(
database={'name': 'all'},
config={},
dump_path=object,
database_names=('foo', 'bar'),
extra_environment=object,
environment={'USER': 'root'},
dry_run=object,
dry_run_label=object,
).and_return(process).once()
@@ -141,14 +147,16 @@ 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).should_receive('database_names_to_dump').and_return(('foo', 'bar'))
for name, process in zip(('foo', 'bar'), processes):
flexmock(module).should_receive('execute_dump_command').with_args(
database={'name': name, 'format': 'sql'},
config={},
dump_path=object,
database_names=(name,),
extra_environment=object,
environment={'USER': 'root'},
dry_run=object,
dry_run_label=object,
).and_return(process).once()
@@ -177,10 +185,10 @@ def test_database_names_to_dump_runs_mysql_with_list_options():
'--execute',
'show schemas',
),
extra_environment=None,
environment=None,
).and_return(('foo\nbar')).once()
assert module.database_names_to_dump(database, None, '') == ('foo', 'bar')
assert module.database_names_to_dump(database, {}, None, '') == ('foo', 'bar')
def test_database_names_to_dump_runs_non_default_mysql_with_list_options():
@@ -190,7 +198,7 @@ def test_database_names_to_dump_runs_non_default_mysql_with_list_options():
'mysql_command': 'custom_mysql',
}
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
extra_environment=None,
environment=None,
full_command=(
'custom_mysql', # Custom MySQL command
'--defaults-extra-file=my.cnf',
@@ -201,7 +209,7 @@ def test_database_names_to_dump_runs_non_default_mysql_with_list_options():
),
).and_return(('foo\nbar')).once()
assert module.database_names_to_dump(database, None, '') == ('foo', 'bar')
assert module.database_names_to_dump(database, {}, None, '') == ('foo', 'bar')
def test_execute_dump_command_runs_mysqldump():
@@ -210,7 +218,7 @@ def test_execute_dump_command_runs_mysqldump():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -222,16 +230,17 @@ def test_execute_dump_command_runs_mysqldump():
'--result-file',
'dump',
),
extra_environment=None,
environment=None,
run_to_completion=False,
).and_return(process).once()
assert (
module.execute_dump_command(
database={'name': 'foo'},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
environment=None,
dry_run=False,
dry_run_label='',
)
@@ -245,7 +254,7 @@ def test_execute_dump_command_runs_mysqldump_without_add_drop_database():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -256,16 +265,17 @@ def test_execute_dump_command_runs_mysqldump_without_add_drop_database():
'--result-file',
'dump',
),
extra_environment=None,
environment=None,
run_to_completion=False,
).and_return(process).once()
assert (
module.execute_dump_command(
database={'name': 'foo', 'add_drop_database': False},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
environment=None,
dry_run=False,
dry_run_label='',
)
@@ -279,7 +289,7 @@ def test_execute_dump_command_runs_mysqldump_with_hostname_and_port():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -297,16 +307,17 @@ def test_execute_dump_command_runs_mysqldump_with_hostname_and_port():
'--result-file',
'dump',
),
extra_environment=None,
environment=None,
run_to_completion=False,
).and_return(process).once()
assert (
module.execute_dump_command(
database={'name': 'foo', 'hostname': 'database.example.org', 'port': 5433},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
environment=None,
dry_run=False,
dry_run_label='',
)
@@ -320,7 +331,7 @@ def test_execute_dump_command_runs_mysqldump_with_username_and_password():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -334,16 +345,17 @@ def test_execute_dump_command_runs_mysqldump_with_username_and_password():
'--result-file',
'dump',
),
extra_environment={'MYSQL_PWD': 'trustsome1'},
environment={'MYSQL_PWD': 'trustsome1'},
run_to_completion=False,
).and_return(process).once()
assert (
module.execute_dump_command(
database={'name': 'foo', 'username': 'root', 'password': 'trustsome1'},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment={'MYSQL_PWD': 'trustsome1'},
environment={'MYSQL_PWD': 'trustsome1'},
dry_run=False,
dry_run_label='',
)
@@ -357,7 +369,7 @@ def test_execute_dump_command_runs_mysqldump_with_options():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -370,16 +382,17 @@ def test_execute_dump_command_runs_mysqldump_with_options():
'--result-file',
'dump',
),
extra_environment=None,
environment=None,
run_to_completion=False,
).and_return(process).once()
assert (
module.execute_dump_command(
database={'name': 'foo', 'options': '--stuff=such'},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
environment=None,
dry_run=False,
dry_run_label='',
)
@@ -393,7 +406,7 @@ def test_execute_dump_command_runs_non_default_mysqldump():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -405,7 +418,7 @@ def test_execute_dump_command_runs_non_default_mysqldump():
'--result-file',
'dump',
),
extra_environment=None,
environment=None,
run_to_completion=False,
).and_return(process).once()
@@ -415,9 +428,10 @@ def test_execute_dump_command_runs_non_default_mysqldump():
'name': 'foo',
'mysql_dump_command': 'custom_mysqldump',
}, # Custom MySQL dump command specified
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
environment=None,
dry_run=False,
dry_run_label='',
)
@@ -434,9 +448,10 @@ def test_execute_dump_command_with_duplicate_dump_skips_mysqldump():
assert (
module.execute_dump_command(
database={'name': 'foo'},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
environment=None,
dry_run=True,
dry_run_label='SO DRY',
)
@@ -449,7 +464,7 @@ def test_execute_dump_command_with_dry_run_skips_mysqldump():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').never()
@@ -457,9 +472,10 @@ def test_execute_dump_command_with_dry_run_skips_mysqldump():
assert (
module.execute_dump_command(
database={'name': 'foo'},
config={},
dump_path=flexmock(),
database_names=('foo',),
extra_environment=None,
environment=None,
dry_run=True,
dry_run_label='SO DRY',
)
@@ -470,9 +486,10 @@ def test_execute_dump_command_with_dry_run_skips_mysqldump():
def test_dump_data_sources_errors_for_missing_all_databases():
databases = [{'name': 'all'}]
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
'databases/localhost/all'
)
@@ -492,9 +509,10 @@ def test_dump_data_sources_errors_for_missing_all_databases():
def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run():
databases = [{'name': 'all'}]
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
'databases/localhost/all'
)
@@ -519,13 +537,14 @@ def test_restore_data_source_dump_runs_mysql_to_restore():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
('mysql', '--batch'),
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment=None,
environment={'USER': 'root'},
).once()
module.restore_data_source_dump(
@@ -550,13 +569,14 @@ def test_restore_data_source_dump_runs_mysql_with_options():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
('mysql', '--batch', '--harder'),
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment=None,
environment={'USER': 'root'},
).once()
module.restore_data_source_dump(
@@ -581,13 +601,14 @@ def test_restore_data_source_dump_runs_non_default_mysql_with_options():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
('custom_mysql', '--batch', '--harder'),
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment=None,
environment={'USER': 'root'},
).once()
module.restore_data_source_dump(
@@ -612,7 +633,8 @@ def test_restore_data_source_dump_runs_mysql_with_hostname_and_port():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mysql',
@@ -627,7 +649,7 @@ def test_restore_data_source_dump_runs_mysql_with_hostname_and_port():
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment=None,
environment={'USER': 'root'},
).once()
module.restore_data_source_dump(
@@ -652,13 +674,14 @@ def test_restore_data_source_dump_runs_mysql_with_username_and_password():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
('mysql', '--batch', '--user', 'root'),
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment={'MYSQL_PWD': 'trustsome1'},
environment={'USER': 'root', 'MYSQL_PWD': 'trustsome1'},
).once()
module.restore_data_source_dump(
@@ -693,7 +716,8 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mysql',
@@ -710,7 +734,7 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment={'MYSQL_PWD': 'clipassword'},
environment={'USER': 'root', 'MYSQL_PWD': 'clipassword'},
).once()
module.restore_data_source_dump(
@@ -747,7 +771,8 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mysql',
@@ -764,7 +789,7 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment={'MYSQL_PWD': 'restorepass'},
environment={'USER': 'root', 'MYSQL_PWD': 'restorepass'},
).once()
module.restore_data_source_dump(
@@ -788,7 +813,8 @@ def test_restore_data_source_dump_with_dry_run_skips_restore():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module).should_receive('execute_command_with_processes').never()
module.restore_data_source_dump(
+114 -113
View File
@@ -6,7 +6,7 @@ from flexmock import flexmock
from borgmatic.hooks.data_source import postgresql as module
def test_make_extra_environment_maps_options_to_environment():
def test_make_environment_maps_options_to_environment():
database = {
'name': 'foo',
'password': 'pass',
@@ -17,6 +17,7 @@ def test_make_extra_environment_maps_options_to_environment():
'ssl_crl': 'crl.crl',
}
expected = {
'USER': 'root',
'PGPASSWORD': 'pass',
'PGSSLMODE': 'require',
'PGSSLCERT': 'cert.crt',
@@ -24,84 +25,84 @@ def test_make_extra_environment_maps_options_to_environment():
'PGSSLROOTCERT': 'root.crt',
'PGSSLCRL': 'crl.crl',
}
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
extra_env = module.make_extra_environment(database)
assert extra_env == expected
assert module.make_environment(database, {}) == expected
def test_make_extra_environment_with_cli_password_sets_correct_password():
def test_make_environment_with_cli_password_sets_correct_password():
database = {'name': 'foo', 'restore_password': 'trustsome1', 'password': 'anotherpassword'}
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
extra = module.make_extra_environment(
database, restore_connection_params={'password': 'clipassword'}
environment = module.make_environment(
database, {}, restore_connection_params={'password': 'clipassword'}
)
assert extra['PGPASSWORD'] == 'clipassword'
assert environment['PGPASSWORD'] == 'clipassword'
def test_make_extra_environment_without_cli_password_or_configured_password_does_not_set_password():
def test_make_environment_without_cli_password_or_configured_password_does_not_set_password():
database = {'name': 'foo'}
extra = module.make_extra_environment(
database, restore_connection_params={'username': 'someone'}
environment = module.make_environment(
database, {}, restore_connection_params={'username': 'someone'}
)
assert 'PGPASSWORD' not in extra
assert 'PGPASSWORD' not in environment
def test_make_extra_environment_without_ssl_mode_does_not_set_ssl_mode():
def test_make_environment_without_ssl_mode_does_not_set_ssl_mode():
database = {'name': 'foo'}
extra = module.make_extra_environment(database)
environment = module.make_environment(database, {})
assert 'PGSSLMODE' not in extra
assert 'PGSSLMODE' not in environment
def test_database_names_to_dump_passes_through_individual_database_name():
database = {'name': 'foo'}
assert module.database_names_to_dump(database, flexmock(), dry_run=False) == ('foo',)
assert module.database_names_to_dump(database, {}, flexmock(), dry_run=False) == ('foo',)
def test_database_names_to_dump_passes_through_individual_database_name_with_format():
database = {'name': 'foo', 'format': 'custom'}
assert module.database_names_to_dump(database, flexmock(), dry_run=False) == ('foo',)
assert module.database_names_to_dump(database, {}, flexmock(), dry_run=False) == ('foo',)
def test_database_names_to_dump_passes_through_all_without_format():
database = {'name': 'all'}
assert module.database_names_to_dump(database, flexmock(), dry_run=False) == ('all',)
assert module.database_names_to_dump(database, {}, flexmock(), dry_run=False) == ('all',)
def test_database_names_to_dump_with_all_and_format_and_dry_run_bails():
database = {'name': 'all', 'format': 'custom'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_and_capture_output').never()
assert module.database_names_to_dump(database, flexmock(), dry_run=True) == ()
assert module.database_names_to_dump(database, {}, flexmock(), dry_run=True) == ()
def test_database_names_to_dump_with_all_and_format_lists_databases():
database = {'name': 'all', 'format': 'custom'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
'foo,test,\nbar,test,"stuff and such"'
)
assert module.database_names_to_dump(database, flexmock(), dry_run=False) == (
assert module.database_names_to_dump(database, {}, flexmock(), dry_run=False) == (
'foo',
'bar',
)
@@ -111,7 +112,7 @@ def test_database_names_to_dump_with_all_and_format_lists_databases_with_hostnam
database = {'name': 'all', 'format': 'custom', 'hostname': 'localhost', 'port': 1234}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
(
'psql',
@@ -125,10 +126,10 @@ def test_database_names_to_dump_with_all_and_format_lists_databases_with_hostnam
'--port',
'1234',
),
extra_environment=object,
environment=object,
).and_return('foo,test,\nbar,test,"stuff and such"')
assert module.database_names_to_dump(database, flexmock(), dry_run=False) == (
assert module.database_names_to_dump(database, {}, flexmock(), dry_run=False) == (
'foo',
'bar',
)
@@ -138,7 +139,7 @@ def test_database_names_to_dump_with_all_and_format_lists_databases_with_usernam
database = {'name': 'all', 'format': 'custom', 'username': 'postgres'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
(
'psql',
@@ -150,10 +151,10 @@ def test_database_names_to_dump_with_all_and_format_lists_databases_with_usernam
'--username',
'postgres',
),
extra_environment=object,
environment=object,
).and_return('foo,test,\nbar,test,"stuff and such"')
assert module.database_names_to_dump(database, flexmock(), dry_run=False) == (
assert module.database_names_to_dump(database, {}, flexmock(), dry_run=False) == (
'foo',
'bar',
)
@@ -163,13 +164,13 @@ def test_database_names_to_dump_with_all_and_format_lists_databases_with_options
database = {'name': 'all', 'format': 'custom', 'list_options': '--harder'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('psql', '--list', '--no-password', '--no-psqlrc', '--csv', '--tuples-only', '--harder'),
extra_environment=object,
environment=object,
).and_return('foo,test,\nbar,test,"stuff and such"')
assert module.database_names_to_dump(database, flexmock(), dry_run=False) == (
assert module.database_names_to_dump(database, {}, flexmock(), dry_run=False) == (
'foo',
'bar',
)
@@ -179,12 +180,12 @@ def test_database_names_to_dump_with_all_and_format_excludes_particular_database
database = {'name': 'all', 'format': 'custom'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
'foo,test,\ntemplate0,test,blah'
)
assert module.database_names_to_dump(database, flexmock(), dry_run=False) == ('foo',)
assert module.database_names_to_dump(database, {}, flexmock(), dry_run=False) == ('foo',)
def test_database_names_to_dump_with_all_and_psql_command_uses_custom_command():
@@ -195,7 +196,7 @@ def test_database_names_to_dump_with_all_and_psql_command_uses_custom_command():
}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
(
'docker',
@@ -210,10 +211,10 @@ def test_database_names_to_dump_with_all_and_psql_command_uses_custom_command():
'--csv',
'--tuples-only',
),
extra_environment=object,
environment=object,
).and_return('foo,text').once()
assert module.database_names_to_dump(database, flexmock(), dry_run=False) == ('foo',)
assert module.database_names_to_dump(database, {}, flexmock(), dry_run=False) == ('foo',)
def test_use_streaming_true_for_any_non_directory_format_databases():
@@ -237,7 +238,7 @@ def test_use_streaming_false_for_no_databases():
def test_dump_data_sources_runs_pg_dump_for_each_database():
databases = [{'name': 'foo'}, {'name': 'bar'}]
processes = [flexmock(), flexmock()]
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
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',)).and_return(
('bar',)
@@ -248,7 +249,7 @@ def test_dump_data_sources_runs_pg_dump_for_each_database():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
for name, process in zip(('foo', 'bar'), processes):
@@ -265,7 +266,7 @@ def test_dump_data_sources_runs_pg_dump_for_each_database():
f'databases/localhost/{name}',
),
shell=True,
extra_environment={'PGSSLMODE': 'disable'},
environment={'PGSSLMODE': 'disable'},
run_to_completion=False,
).and_return(process).once()
@@ -284,7 +285,7 @@ def test_dump_data_sources_runs_pg_dump_for_each_database():
def test_dump_data_sources_raises_when_no_database_names_to_dump():
databases = [{'name': 'foo'}, {'name': 'bar'}]
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
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(())
@@ -301,7 +302,7 @@ def test_dump_data_sources_raises_when_no_database_names_to_dump():
def test_dump_data_sources_does_not_raise_when_no_database_names_to_dump():
databases = [{'name': 'foo'}, {'name': 'bar'}]
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
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(())
@@ -317,7 +318,7 @@ def test_dump_data_sources_does_not_raise_when_no_database_names_to_dump():
def test_dump_data_sources_with_duplicate_dump_skips_pg_dump():
databases = [{'name': 'foo'}, {'name': 'bar'}]
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
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',)).and_return(
('bar',)
@@ -344,7 +345,7 @@ def test_dump_data_sources_with_duplicate_dump_skips_pg_dump():
def test_dump_data_sources_with_dry_run_skips_pg_dump():
databases = [{'name': 'foo'}, {'name': 'bar'}]
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
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',)).and_return(
('bar',)
@@ -355,7 +356,7 @@ def test_dump_data_sources_with_dry_run_skips_pg_dump():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
flexmock(module).should_receive('execute_command').never()
@@ -375,7 +376,7 @@ def test_dump_data_sources_with_dry_run_skips_pg_dump():
def test_dump_data_sources_runs_pg_dump_with_hostname_and_port():
databases = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
process = flexmock()
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
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(
@@ -384,7 +385,7 @@ def test_dump_data_sources_runs_pg_dump_with_hostname_and_port():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -404,7 +405,7 @@ def test_dump_data_sources_runs_pg_dump_with_hostname_and_port():
'databases/database.example.org/foo',
),
shell=True,
extra_environment={'PGSSLMODE': 'disable'},
environment={'PGSSLMODE': 'disable'},
run_to_completion=False,
).and_return(process).once()
@@ -421,7 +422,7 @@ def test_dump_data_sources_runs_pg_dump_with_hostname_and_port():
def test_dump_data_sources_runs_pg_dump_with_username_and_password():
databases = [{'name': 'foo', 'username': 'postgres', 'password': 'trustsome1'}]
process = flexmock()
flexmock(module).should_receive('make_extra_environment').and_return(
flexmock(module).should_receive('make_environment').and_return(
{'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'}
)
flexmock(module).should_receive('make_dump_path').and_return('')
@@ -432,7 +433,7 @@ def test_dump_data_sources_runs_pg_dump_with_username_and_password():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -450,7 +451,7 @@ def test_dump_data_sources_runs_pg_dump_with_username_and_password():
'databases/localhost/foo',
),
shell=True,
extra_environment={'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'},
environment={'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'},
run_to_completion=False,
).and_return(process).once()
@@ -467,7 +468,7 @@ def test_dump_data_sources_runs_pg_dump_with_username_and_password():
def test_dump_data_sources_with_username_injection_attack_gets_escaped():
databases = [{'name': 'foo', 'username': 'postgres; naughty-command', 'password': 'trustsome1'}]
process = flexmock()
flexmock(module).should_receive('make_extra_environment').and_return(
flexmock(module).should_receive('make_environment').and_return(
{'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'}
)
flexmock(module).should_receive('make_dump_path').and_return('')
@@ -478,7 +479,7 @@ def test_dump_data_sources_with_username_injection_attack_gets_escaped():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -496,7 +497,7 @@ def test_dump_data_sources_with_username_injection_attack_gets_escaped():
'databases/localhost/foo',
),
shell=True,
extra_environment={'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'},
environment={'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'},
run_to_completion=False,
).and_return(process).once()
@@ -512,7 +513,7 @@ def test_dump_data_sources_with_username_injection_attack_gets_escaped():
def test_dump_data_sources_runs_pg_dump_with_directory_format():
databases = [{'name': 'foo', 'format': 'directory'}]
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
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(
@@ -521,7 +522,7 @@ def test_dump_data_sources_runs_pg_dump_with_directory_format():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_parent_directory_for_dump')
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
@@ -538,7 +539,7 @@ def test_dump_data_sources_runs_pg_dump_with_directory_format():
'foo',
),
shell=True,
extra_environment={'PGSSLMODE': 'disable'},
environment={'PGSSLMODE': 'disable'},
).and_return(flexmock()).once()
assert (
@@ -557,7 +558,7 @@ def test_dump_data_sources_runs_pg_dump_with_directory_format():
def test_dump_data_sources_runs_pg_dump_with_options():
databases = [{'name': 'foo', 'options': '--stuff=such'}]
process = flexmock()
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
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(
@@ -566,7 +567,7 @@ def test_dump_data_sources_runs_pg_dump_with_options():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -583,7 +584,7 @@ def test_dump_data_sources_runs_pg_dump_with_options():
'databases/localhost/foo',
),
shell=True,
extra_environment={'PGSSLMODE': 'disable'},
environment={'PGSSLMODE': 'disable'},
run_to_completion=False,
).and_return(process).once()
@@ -600,7 +601,7 @@ def test_dump_data_sources_runs_pg_dump_with_options():
def test_dump_data_sources_runs_pg_dumpall_for_all_databases():
databases = [{'name': 'all'}]
process = flexmock()
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
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(('all',))
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
@@ -609,13 +610,13 @@ def test_dump_data_sources_runs_pg_dumpall_for_all_databases():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
('pg_dumpall', '--no-password', '--clean', '--if-exists', '>', 'databases/localhost/all'),
shell=True,
extra_environment={'PGSSLMODE': 'disable'},
environment={'PGSSLMODE': 'disable'},
run_to_completion=False,
).and_return(process).once()
@@ -632,7 +633,7 @@ def test_dump_data_sources_runs_pg_dumpall_for_all_databases():
def test_dump_data_sources_runs_non_default_pg_dump():
databases = [{'name': 'foo', 'pg_dump_command': 'special_pg_dump --compress *'}]
process = flexmock()
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
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(
@@ -641,7 +642,7 @@ def test_dump_data_sources_runs_non_default_pg_dump():
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
).replace_with(lambda value, config: value)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
@@ -659,7 +660,7 @@ def test_dump_data_sources_runs_non_default_pg_dump():
'databases/localhost/foo',
),
shell=True,
extra_environment={'PGSSLMODE': 'disable'},
environment={'PGSSLMODE': 'disable'},
run_to_completion=False,
).and_return(process).once()
@@ -679,8 +680,8 @@ def test_restore_data_source_dump_runs_pg_restore():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
).replace_with(lambda value, config: value)
flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_data_source_dump_filename')
flexmock(module).should_receive('execute_command_with_processes').with_args(
@@ -696,7 +697,7 @@ def test_restore_data_source_dump_runs_pg_restore():
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment={'PGSSLMODE': 'disable'},
environment={'PGSSLMODE': 'disable'},
).once()
flexmock(module).should_receive('execute_command').with_args(
(
@@ -709,7 +710,7 @@ def test_restore_data_source_dump_runs_pg_restore():
'--command',
'ANALYZE',
),
extra_environment={'PGSSLMODE': 'disable'},
environment={'PGSSLMODE': 'disable'},
).once()
module.restore_data_source_dump(
@@ -736,8 +737,8 @@ def test_restore_data_source_dump_runs_pg_restore_with_hostname_and_port():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
).replace_with(lambda value, config: value)
flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_data_source_dump_filename')
flexmock(module).should_receive('execute_command_with_processes').with_args(
@@ -757,7 +758,7 @@ def test_restore_data_source_dump_runs_pg_restore_with_hostname_and_port():
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment={'PGSSLMODE': 'disable'},
environment={'PGSSLMODE': 'disable'},
).once()
flexmock(module).should_receive('execute_command').with_args(
(
@@ -774,7 +775,7 @@ def test_restore_data_source_dump_runs_pg_restore_with_hostname_and_port():
'--command',
'ANALYZE',
),
extra_environment={'PGSSLMODE': 'disable'},
environment={'PGSSLMODE': 'disable'},
).once()
module.restore_data_source_dump(
@@ -801,8 +802,8 @@ def test_restore_data_source_dump_runs_pg_restore_with_username_and_password():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('make_extra_environment').and_return(
).replace_with(lambda value, config: value)
flexmock(module).should_receive('make_environment').and_return(
{'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'}
)
flexmock(module).should_receive('make_dump_path')
@@ -822,7 +823,7 @@ def test_restore_data_source_dump_runs_pg_restore_with_username_and_password():
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment={'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'},
environment={'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'},
).once()
flexmock(module).should_receive('execute_command').with_args(
(
@@ -837,7 +838,7 @@ def test_restore_data_source_dump_runs_pg_restore_with_username_and_password():
'--command',
'ANALYZE',
),
extra_environment={'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'},
environment={'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'},
).once()
module.restore_data_source_dump(
@@ -875,8 +876,8 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('make_extra_environment').and_return(
).replace_with(lambda value, config: value)
flexmock(module).should_receive('make_environment').and_return(
{'PGPASSWORD': 'clipassword', 'PGSSLMODE': 'disable'}
)
flexmock(module).should_receive('make_dump_path')
@@ -900,7 +901,7 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment={'PGPASSWORD': 'clipassword', 'PGSSLMODE': 'disable'},
environment={'PGPASSWORD': 'clipassword', 'PGSSLMODE': 'disable'},
).once()
flexmock(module).should_receive('execute_command').with_args(
(
@@ -919,7 +920,7 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
'--command',
'ANALYZE',
),
extra_environment={'PGPASSWORD': 'clipassword', 'PGSSLMODE': 'disable'},
environment={'PGPASSWORD': 'clipassword', 'PGSSLMODE': 'disable'},
).once()
module.restore_data_source_dump(
@@ -957,8 +958,8 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('make_extra_environment').and_return(
).replace_with(lambda value, config: value)
flexmock(module).should_receive('make_environment').and_return(
{'PGPASSWORD': 'restorepassword', 'PGSSLMODE': 'disable'}
)
flexmock(module).should_receive('make_dump_path')
@@ -982,7 +983,7 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment={'PGPASSWORD': 'restorepassword', 'PGSSLMODE': 'disable'},
environment={'PGPASSWORD': 'restorepassword', 'PGSSLMODE': 'disable'},
).once()
flexmock(module).should_receive('execute_command').with_args(
(
@@ -1001,7 +1002,7 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
'--command',
'ANALYZE',
),
extra_environment={'PGPASSWORD': 'restorepassword', 'PGSSLMODE': 'disable'},
environment={'PGPASSWORD': 'restorepassword', 'PGSSLMODE': 'disable'},
).once()
module.restore_data_source_dump(
@@ -1033,8 +1034,8 @@ def test_restore_data_source_dump_runs_pg_restore_with_options():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
).replace_with(lambda value, config: value)
flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_data_source_dump_filename')
flexmock(module).should_receive('execute_command_with_processes').with_args(
@@ -1051,7 +1052,7 @@ def test_restore_data_source_dump_runs_pg_restore_with_options():
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment={'PGSSLMODE': 'disable'},
environment={'PGSSLMODE': 'disable'},
).once()
flexmock(module).should_receive('execute_command').with_args(
(
@@ -1065,7 +1066,7 @@ def test_restore_data_source_dump_runs_pg_restore_with_options():
'--command',
'ANALYZE',
),
extra_environment={'PGSSLMODE': 'disable'},
environment={'PGSSLMODE': 'disable'},
).once()
module.restore_data_source_dump(
@@ -1090,8 +1091,8 @@ def test_restore_data_source_dump_runs_psql_for_all_database_dump():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
).replace_with(lambda value, config: value)
flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_data_source_dump_filename')
flexmock(module).should_receive('execute_command_with_processes').with_args(
@@ -1103,11 +1104,11 @@ def test_restore_data_source_dump_runs_psql_for_all_database_dump():
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment={'PGSSLMODE': 'disable'},
environment={'PGSSLMODE': 'disable'},
).once()
flexmock(module).should_receive('execute_command').with_args(
('psql', '--no-password', '--no-psqlrc', '--quiet', '--command', 'ANALYZE'),
extra_environment={'PGSSLMODE': 'disable'},
environment={'PGSSLMODE': 'disable'},
).once()
module.restore_data_source_dump(
@@ -1132,8 +1133,8 @@ def test_restore_data_source_dump_runs_psql_for_plain_database_dump():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
).replace_with(lambda value, config: value)
flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_data_source_dump_filename')
flexmock(module).should_receive('execute_command_with_processes').with_args(
@@ -1141,7 +1142,7 @@ def test_restore_data_source_dump_runs_psql_for_plain_database_dump():
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment={'PGSSLMODE': 'disable'},
environment={'PGSSLMODE': 'disable'},
).once()
flexmock(module).should_receive('execute_command').with_args(
(
@@ -1154,7 +1155,7 @@ def test_restore_data_source_dump_runs_psql_for_plain_database_dump():
'--command',
'ANALYZE',
),
extra_environment={'PGSSLMODE': 'disable'},
environment={'PGSSLMODE': 'disable'},
).once()
module.restore_data_source_dump(
@@ -1186,8 +1187,8 @@ def test_restore_data_source_dump_runs_non_default_pg_restore_and_psql():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
).replace_with(lambda value, config: value)
flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_data_source_dump_filename')
flexmock(module).should_receive('execute_command_with_processes').with_args(
@@ -1208,7 +1209,7 @@ def test_restore_data_source_dump_runs_non_default_pg_restore_and_psql():
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment={'PGSSLMODE': 'disable'},
environment={'PGSSLMODE': 'disable'},
).once()
flexmock(module).should_receive('execute_command').with_args(
(
@@ -1226,7 +1227,7 @@ def test_restore_data_source_dump_runs_non_default_pg_restore_and_psql():
'--command',
'ANALYZE',
),
extra_environment={'PGSSLMODE': 'disable'},
environment={'PGSSLMODE': 'disable'},
).once()
module.restore_data_source_dump(
@@ -1250,8 +1251,8 @@ def test_restore_data_source_dump_with_dry_run_skips_restore():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
).replace_with(lambda value, config: value)
flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_data_source_dump_filename')
flexmock(module).should_receive('execute_command_with_processes').never()
@@ -1277,8 +1278,8 @@ def test_restore_data_source_dump_without_extract_process_restores_from_disk():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
).replace_with(lambda value, config: value)
flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('/dump/path')
flexmock(module).should_receive('execute_command_with_processes').with_args(
@@ -1295,7 +1296,7 @@ def test_restore_data_source_dump_without_extract_process_restores_from_disk():
processes=[],
output_log_level=logging.DEBUG,
input_file=None,
extra_environment={'PGSSLMODE': 'disable'},
environment={'PGSSLMODE': 'disable'},
).once()
flexmock(module).should_receive('execute_command').with_args(
(
@@ -1308,7 +1309,7 @@ def test_restore_data_source_dump_without_extract_process_restores_from_disk():
'--command',
'ANALYZE',
),
extra_environment={'PGSSLMODE': 'disable'},
environment={'PGSSLMODE': 'disable'},
).once()
module.restore_data_source_dump(
@@ -1332,8 +1333,8 @@ def test_restore_data_source_dump_with_schemas_restores_schemas():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value: value)
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
).replace_with(lambda value, config: value)
flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('/dump/path')
flexmock(module).should_receive('execute_command_with_processes').with_args(
@@ -1354,7 +1355,7 @@ def test_restore_data_source_dump_with_schemas_restores_schemas():
processes=[],
output_log_level=logging.DEBUG,
input_file=None,
extra_environment={'PGSSLMODE': 'disable'},
environment={'PGSSLMODE': 'disable'},
).once()
flexmock(module).should_receive('execute_command').with_args(
(
@@ -1367,7 +1368,7 @@ def test_restore_data_source_dump_with_schemas_restores_schemas():
'--command',
'ANALYZE',
),
extra_environment={'PGSSLMODE': 'disable'},
environment={'PGSSLMODE': 'disable'},
).once()
module.restore_data_source_dump(

Some files were not shown because too many files have changed in this diff Show More