mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-22 10:13:00 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a33e5fa1e | ||
|
|
c2d2ce18a2 | ||
|
|
1b62be0949 | ||
|
|
392fbcd19f | ||
|
|
0d54fda27a | ||
|
|
1e6fba6e99 | ||
|
|
a1b14d58af | ||
|
|
6058efbaac | ||
|
|
05b34d4b56 | ||
|
|
5f6173ab24 | ||
|
|
6f92ae2462 | ||
|
|
df383be8a2 | ||
|
|
11da378304 | ||
|
|
65ce7309f1 | ||
|
|
0cae581014 | ||
|
|
123f8958f6 | ||
|
|
5ac086fa73 | ||
|
|
3902dd873d | ||
|
|
652a63223a | ||
|
|
01dc7e7841 | ||
|
|
249183250b | ||
|
|
080c8dfd93 | ||
|
|
9ec287cd2e | ||
|
|
df64794115 | ||
|
|
fede523dae | ||
|
|
35bfb393c7 | ||
|
|
782f534636 | ||
|
|
8e8d9e6267 | ||
|
|
b208c5352a | ||
|
|
79cf3495ee | ||
|
|
15607d2ccd | ||
|
|
ae1fbf7940 | ||
|
|
a1eb612a08 | ||
|
|
7f3e9c5347 | ||
|
|
d83c444d9e | ||
|
|
2221be4549 | ||
|
|
9973c5fb7d | ||
|
|
ec7101ccf3 | ||
|
|
5966022b67 | ||
|
|
b68211cc0c | ||
|
|
1f773124f6 | ||
|
|
3ff9a33468 | ||
|
|
02644c3bb6 | ||
|
|
0e42ba21ba | ||
|
|
d876e8636a | ||
|
|
3127da54b5 | ||
|
|
3d44e95c1a |
@@ -1,6 +1,10 @@
|
||||
## Hold up!
|
||||
## Hold up, GitHub users
|
||||
|
||||
Thanks for your contribution. Unfortunately, we don't use GitHub pull requests to manage code contributions to this repository (and GitHub doesn't have any way to disable pull requests entirely). Instead, please see:
|
||||
Thanks for your contribution!
|
||||
|
||||
If you're submitting this PR on **projects.torsion.org**: Please ignore the following. You can safely delete this text before submitting your PR.
|
||||
|
||||
If you're submitting this PR on **GitHub**: Unfortunately, we don't use GitHub pull requests to manage code contributions to this repository (and GitHub doesn't have any way to disable pull requests entirely). Instead, please see:
|
||||
|
||||
https://torsion.org/borgmatic/#contributing
|
||||
|
||||
|
||||
@@ -1,3 +1,37 @@
|
||||
2.0.7
|
||||
* #1032: Fix a bug in which a Borg archive gets created even when a database hook fails.
|
||||
* #1043: Support Btrfs subvolume paths in "source_directories" even when the subvolume is mounted
|
||||
elsewhere.
|
||||
* #1048: Ignore Btrfs subvolumes whose read-only status can't be determined.
|
||||
* #1083: Add "debug_passphrase"/"display_passphrase" options and a "{unixtime}" placeholder in
|
||||
support of Borg 2 features.
|
||||
* #1099: Clarify documentation on command hooks order of execution.
|
||||
* #1100: Fix a bug in which "borg --version" failing isn't considered a "fail" state in a command
|
||||
hook.
|
||||
* #1108: Fix a bug in which quoted "extra_borg_options" values containing spaces are passed to Borg
|
||||
incorrectly.
|
||||
* #1108: Add a "--comment" flag to the "create" action for creating an archive with a comment.
|
||||
* Use the Bandit security analysis tool when running tests.
|
||||
* SECURITY: Add timeouts to all monitoring hooks to prevent hangs on network requests, e.g. due to
|
||||
a compromised monitoring server holding requests open.
|
||||
* SECURITY: For the "spot" check, use a more secure source of randomness when selecting paths to
|
||||
check.
|
||||
|
||||
2.0.6
|
||||
* #1068: Fix a warning from LVM about leaked file descriptors.
|
||||
* #1086: Fix for the "spot" check breaking when the "--progress" flag is used.
|
||||
* #1089: Fix for the "spot" check erroring when a checksum command errors.
|
||||
* #1091: Fix for the "config generate" action generating invalid configuration when upgrading
|
||||
deprecated command hooks.
|
||||
* #1093: Fix for the LVM hook erroring when the "--dry-run" flag is used.
|
||||
* #1094: Fix incorrect documentation about customizing Borg exit codes:
|
||||
https://torsion.org/borgmatic/docs/how-to/customize-warnings-and-errors/
|
||||
* #1095: Fix for the "spot" check's "xxh64sum_command" option erroring on commands containing
|
||||
spaces.
|
||||
* Add support for Borg 2's "s3:" and "b2:" repository URLs, so you can backup to S3 or B2 cloud
|
||||
storage services even without using Rclone.
|
||||
* During the "spot" check, truncate log messages containing many file paths.
|
||||
|
||||
2.0.5
|
||||
* #1033: Add a "password_transport" option to the MariaDB and MySQL database hooks for customizing
|
||||
how borgmatic transmits passwords to the database client.
|
||||
|
||||
+40
-10
@@ -6,7 +6,9 @@ import logging
|
||||
import os
|
||||
import pathlib
|
||||
import random
|
||||
import shlex
|
||||
import shutil
|
||||
import textwrap
|
||||
|
||||
import borgmatic.actions.pattern
|
||||
import borgmatic.borg.check
|
||||
@@ -372,7 +374,11 @@ def collect_spot_check_source_paths(
|
||||
borgmatic.borg.create.make_base_create_command(
|
||||
dry_run=True,
|
||||
repository_path=repository['path'],
|
||||
config=dict(config, list_details=True),
|
||||
# Omit "progress" because it interferes with "list_details".
|
||||
config=dict(
|
||||
{option: value for option, value in config.items() if option != 'progress'},
|
||||
list_details=True,
|
||||
),
|
||||
patterns=borgmatic.actions.pattern.process_patterns(
|
||||
borgmatic.actions.pattern.collect_patterns(config),
|
||||
working_directory,
|
||||
@@ -454,7 +460,7 @@ def collect_spot_check_archive_paths(
|
||||
)
|
||||
|
||||
|
||||
SAMPLE_PATHS_SUBSET_COUNT = 10000
|
||||
SAMPLE_PATHS_SUBSET_COUNT = 5000
|
||||
|
||||
|
||||
def compare_spot_check_hashes(
|
||||
@@ -480,7 +486,7 @@ def compare_spot_check_hashes(
|
||||
sample_count = max(
|
||||
int(len(source_paths) * (min(spot_check_config['data_sample_percentage'], 100) / 100)), 1
|
||||
)
|
||||
source_sample_paths = tuple(random.sample(source_paths, sample_count))
|
||||
source_sample_paths = tuple(random.SystemRandom().sample(source_paths, sample_count))
|
||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
||||
hashable_source_sample_path = {
|
||||
source_path
|
||||
@@ -508,7 +514,10 @@ def compare_spot_check_hashes(
|
||||
break
|
||||
|
||||
hash_output = borgmatic.execute.execute_command_and_capture_output(
|
||||
(spot_check_config.get('xxh64sum_command', 'xxh64sum'),)
|
||||
tuple(
|
||||
shlex.quote(part)
|
||||
for part in shlex.split(spot_check_config.get('xxh64sum_command', 'xxh64sum'))
|
||||
)
|
||||
+ tuple(
|
||||
path for path in source_sample_paths_subset if path in hashable_source_sample_path
|
||||
),
|
||||
@@ -562,6 +571,9 @@ def compare_spot_check_hashes(
|
||||
return tuple(failing_paths)
|
||||
|
||||
|
||||
MAX_SPOT_CHECK_PATHS_LENGTH = 1000
|
||||
|
||||
|
||||
def spot_check(
|
||||
repository,
|
||||
config,
|
||||
@@ -629,9 +641,12 @@ def spot_check(
|
||||
logger.debug(f'{len(archive_paths)} total archive paths for spot check')
|
||||
|
||||
if len(source_paths) == 0:
|
||||
logger.debug(
|
||||
f'Paths in latest archive but not source paths: {", ".join(set(archive_paths)) or "none"}'
|
||||
truncated_archive_paths = textwrap.shorten(
|
||||
', '.join(set(archive_paths)) or 'none',
|
||||
width=MAX_SPOT_CHECK_PATHS_LENGTH,
|
||||
placeholder=' ...',
|
||||
)
|
||||
logger.debug(f'Paths in latest archive but not source paths: {truncated_archive_paths}')
|
||||
raise ValueError(
|
||||
'Spot check failed: There are no source paths to compare against the archive'
|
||||
)
|
||||
@@ -642,11 +657,21 @@ def spot_check(
|
||||
|
||||
if count_delta_percentage > spot_check_config['count_tolerance_percentage']:
|
||||
rootless_source_paths = set(path.lstrip(os.path.sep) for path in source_paths)
|
||||
logger.debug(
|
||||
f'Paths in source paths but not latest archive: {", ".join(rootless_source_paths - set(archive_paths)) or "none"}'
|
||||
truncated_exclusive_source_paths = textwrap.shorten(
|
||||
', '.join(rootless_source_paths - set(archive_paths)) or 'none',
|
||||
width=MAX_SPOT_CHECK_PATHS_LENGTH,
|
||||
placeholder=' ...',
|
||||
)
|
||||
logger.debug(
|
||||
f'Paths in latest archive but not source paths: {", ".join(set(archive_paths) - rootless_source_paths) or "none"}'
|
||||
f'Paths in source paths but not latest archive: {truncated_exclusive_source_paths}'
|
||||
)
|
||||
truncated_exclusive_archive_paths = textwrap.shorten(
|
||||
', '.join(set(archive_paths) - rootless_source_paths) or 'none',
|
||||
width=MAX_SPOT_CHECK_PATHS_LENGTH,
|
||||
placeholder=' ...',
|
||||
)
|
||||
logger.debug(
|
||||
f'Paths in latest archive but not source paths: {truncated_exclusive_archive_paths}'
|
||||
)
|
||||
raise ValueError(
|
||||
f'Spot check failed: {count_delta_percentage:.2f}% file count delta between source paths and latest archive (tolerance is {spot_check_config["count_tolerance_percentage"]}%)'
|
||||
@@ -669,8 +694,13 @@ def spot_check(
|
||||
failing_percentage = (len(failing_paths) / len(source_paths)) * 100
|
||||
|
||||
if failing_percentage > data_tolerance_percentage:
|
||||
truncated_failing_paths = textwrap.shorten(
|
||||
', '.join(failing_paths),
|
||||
width=MAX_SPOT_CHECK_PATHS_LENGTH,
|
||||
placeholder=' ...',
|
||||
)
|
||||
logger.debug(
|
||||
f'Source paths with data not matching the latest archive: {", ".join(failing_paths)}'
|
||||
f'Source paths with data not matching the latest archive: {truncated_failing_paths}'
|
||||
)
|
||||
raise ValueError(
|
||||
f'Spot check failed: {failing_percentage:.2f}% of source paths with data not matching the latest archive (tolerance is {data_tolerance_percentage}%)'
|
||||
|
||||
@@ -2,6 +2,8 @@ import logging
|
||||
|
||||
import borgmatic.actions.json
|
||||
import borgmatic.borg.create
|
||||
import borgmatic.borg.rename
|
||||
import borgmatic.borg.repo_list
|
||||
import borgmatic.config.paths
|
||||
import borgmatic.config.validate
|
||||
import borgmatic.hooks.dispatch
|
||||
@@ -72,6 +74,12 @@ def run_create(
|
||||
)
|
||||
stream_processes = [process for processes in active_dumps.values() for process in processes]
|
||||
|
||||
# If we have stream processes, we first create an archive with .checkpoint suffix. This is
|
||||
# to make sure we only create a real archive if all the streaming processes completed
|
||||
# successfully (create_archive will fail if a streaming process fails, but the archive might
|
||||
# have already been created at this point).
|
||||
use_checkpoint = bool(stream_processes)
|
||||
|
||||
json_output = borgmatic.borg.create.create_archive(
|
||||
global_arguments.dry_run,
|
||||
repository['path'],
|
||||
@@ -80,14 +88,41 @@ def run_create(
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
borgmatic_runtime_directory,
|
||||
archive_suffix='.checkpoint' if use_checkpoint else '',
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
json=create_arguments.json,
|
||||
comment=create_arguments.comment,
|
||||
stream_processes=stream_processes,
|
||||
)
|
||||
|
||||
if use_checkpoint:
|
||||
rename_checkpoint_archive(
|
||||
repository['path'],
|
||||
global_arguments,
|
||||
config,
|
||||
local_borg_version,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
|
||||
if json_output:
|
||||
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
|
||||
output = borgmatic.actions.json.parse_json(json_output, repository.get('label'))
|
||||
if use_checkpoint:
|
||||
# Patch archive name and ID
|
||||
renamed_archive = borgmatic.borg.repo_list.get_latest_archive(
|
||||
repository['path'],
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
|
||||
output['archive']['name'] = renamed_archive['name']
|
||||
output['archive']['id'] = renamed_archive['id']
|
||||
|
||||
yield output
|
||||
|
||||
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||
'remove_data_source_dumps',
|
||||
@@ -96,3 +131,54 @@ def run_create(
|
||||
borgmatic_runtime_directory,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
|
||||
|
||||
def rename_checkpoint_archive(
|
||||
repository_path,
|
||||
global_arguments,
|
||||
config,
|
||||
local_borg_version,
|
||||
local_path,
|
||||
remote_path,
|
||||
):
|
||||
'''
|
||||
Renames the latest archive to not have a '.checkpoint' suffix.
|
||||
|
||||
Raises ValueError if
|
||||
- there is not latest archive
|
||||
- the latest archive does not have a '.checkpoint' suffix
|
||||
|
||||
Implementation note: We cannot reliably get the just created archive name.
|
||||
So we resort to listing the archives and picking the last one.
|
||||
|
||||
A similar comment applies to retrieving the ID of the renamed archive.
|
||||
'''
|
||||
archive = borgmatic.borg.repo_list.get_latest_archive(
|
||||
repository_path,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
consider_checkpoints=True,
|
||||
)
|
||||
|
||||
archive_name = archive['name']
|
||||
|
||||
if not archive_name.endswith('.checkpoint'):
|
||||
raise ValueError(f'Latest archive did not have a .checkpoint suffix. Got: {archive_name}')
|
||||
|
||||
new_archive_name = archive_name.removesuffix('.checkpoint')
|
||||
|
||||
logger.info(f'Renaming archive {archive_name} -> {new_archive_name}')
|
||||
|
||||
borgmatic.borg.rename.rename_archive(
|
||||
repository_path,
|
||||
archive_name,
|
||||
new_archive_name,
|
||||
global_arguments.dry_run,
|
||||
config,
|
||||
local_borg_version,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
|
||||
@@ -60,7 +60,7 @@ def run_arbitrary_borg(
|
||||
return execute_command(
|
||||
tuple(shlex.quote(part) for part in full_command),
|
||||
output_file=DO_NOT_CAPTURE,
|
||||
shell=True,
|
||||
shell=True, # noqa: S604
|
||||
environment=dict(
|
||||
(environment.make_environment(config) or {}),
|
||||
**{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import shlex
|
||||
|
||||
import borgmatic.config.paths
|
||||
from borgmatic.borg import environment, feature, flags, repo_info
|
||||
@@ -171,7 +172,7 @@ def check_archives(
|
||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||
+ verbosity_flags
|
||||
+ (('--progress',) if config.get('progress') else ())
|
||||
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
|
||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import shlex
|
||||
|
||||
import borgmatic.config.paths
|
||||
from borgmatic.borg import environment, feature, flags
|
||||
@@ -42,7 +43,7 @@ def compact_segments(
|
||||
if dry_run and feature.available(feature.Feature.DRY_RUN_COMPACT, local_borg_version)
|
||||
else ()
|
||||
)
|
||||
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
|
||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import shlex
|
||||
import stat
|
||||
import textwrap
|
||||
|
||||
@@ -121,17 +122,21 @@ def make_base_create_command(
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
borgmatic_runtime_directory,
|
||||
archive_suffix='',
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
json=False,
|
||||
comment=None,
|
||||
stream_processes=None,
|
||||
):
|
||||
'''
|
||||
Given verbosity/dry-run flags, a local or remote repository path, a configuration dict, a
|
||||
sequence of patterns as borgmatic.borg.pattern.Pattern instances, the local Borg version,
|
||||
global arguments as an argparse.Namespace instance, and a sequence of borgmatic source
|
||||
directories, return a tuple of (base Borg create command flags, Borg create command positional
|
||||
arguments, open pattern file handle).
|
||||
sequence of patterns as borgmatic.borg.pattern.Pattern instances, the local Borg version, global
|
||||
arguments as an argparse.Namespace instance, the borgmatic runtime directory, a string suffix to
|
||||
add to the archive name, the local Borg path, the remote Borg path, whether to output JSON,
|
||||
comment text to add to the created archive, and a sequence of processes streaming data to Borg,
|
||||
return a tuple of (base Borg create command flags, Borg create command positional arguments,
|
||||
open pattern file handle).
|
||||
'''
|
||||
if config.get('source_directories_must_exist', False):
|
||||
borgmatic.borg.pattern.check_all_root_patterns_exist(patterns)
|
||||
@@ -149,8 +154,9 @@ def make_base_create_command(
|
||||
lock_wait = config.get('lock_wait', None)
|
||||
list_filter_flags = flags.make_list_filter_flags(local_borg_version, dry_run)
|
||||
files_cache = config.get('files_cache')
|
||||
archive_name_format = config.get(
|
||||
'archive_name_format', flags.get_default_archive_name_format(local_borg_version)
|
||||
archive_name_format = (
|
||||
config.get('archive_name_format', flags.get_default_archive_name_format(local_borg_version))
|
||||
+ archive_suffix
|
||||
)
|
||||
extra_borg_options = config.get('extra_borg_options', {}).get('create', '')
|
||||
|
||||
@@ -183,6 +189,7 @@ def make_base_create_command(
|
||||
+ ('create',)
|
||||
+ (('--patterns-from', patterns_file.name) if patterns_file else ())
|
||||
+ flags.make_exclude_flags(config)
|
||||
+ (('--comment', comment) if comment else ())
|
||||
+ (('--checkpoint-interval', str(checkpoint_interval)) if checkpoint_interval else ())
|
||||
+ (('--checkpoint-volume', str(checkpoint_volume)) if checkpoint_volume else ())
|
||||
+ (('--chunker-params', chunker_params) if chunker_params else ())
|
||||
@@ -207,7 +214,7 @@ def make_base_create_command(
|
||||
else ()
|
||||
)
|
||||
+ (('--dry-run',) if dry_run else ())
|
||||
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
|
||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
||||
)
|
||||
|
||||
create_positional_arguments = flags.make_repository_archive_flags(
|
||||
@@ -269,15 +276,20 @@ def create_archive(
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
borgmatic_runtime_directory,
|
||||
archive_suffix='',
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
json=False,
|
||||
comment=None,
|
||||
stream_processes=None,
|
||||
):
|
||||
'''
|
||||
Given verbosity/dry-run flags, a local or remote repository path, a configuration dict, a
|
||||
sequence of loaded configuration paths, the local Borg version, and global arguments as an
|
||||
argparse.Namespace instance, create a Borg archive and return Borg's JSON output (if any).
|
||||
sequence of loaded configuration paths, the local Borg version, global arguments as an
|
||||
argparse.Namespace instance, the borgmatic runtime directory, a string suffix to add to the
|
||||
archive name, the local Borg path, the remote Borg path, whether to output JSON, and comment
|
||||
text to add to the created archive, and a sequence of processes streaming data to Borg, create a
|
||||
Borg archive and return Borg's JSON output (if any).
|
||||
|
||||
If a sequence of stream processes is given (instances of subprocess.Popen), then execute the
|
||||
create command while also triggering the given processes to produce output.
|
||||
@@ -294,9 +306,11 @@ def create_archive(
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
borgmatic_runtime_directory,
|
||||
archive_suffix,
|
||||
local_path,
|
||||
remote_path,
|
||||
json,
|
||||
comment,
|
||||
stream_processes,
|
||||
)
|
||||
|
||||
|
||||
@@ -14,16 +14,18 @@ OPTION_TO_ENVIRONMENT_VARIABLE = {
|
||||
'temporary_directory': 'TMPDIR',
|
||||
}
|
||||
|
||||
DEFAULT_BOOL_OPTION_TO_DOWNCASE_ENVIRONMENT_VARIABLE = {
|
||||
DEFAULT_BOOL_OPTION_TO_UNCONDITIONAL_ENVIRONMENT_VARIABLE = {
|
||||
'check_i_know_what_i_am_doing': 'BORG_CHECK_I_KNOW_WHAT_I_AM_DOING',
|
||||
}
|
||||
|
||||
DEFAULT_BOOL_OPTION_TO_ENVIRONMENT_VARIABLE = {
|
||||
'debug_passphrase': 'BORG_DEBUG_PASSPHRASE',
|
||||
'display_passphrase': 'BORG_DISPLAY_PASSPHRASE',
|
||||
'relocated_repo_access_is_ok': 'BORG_RELOCATED_REPO_ACCESS_IS_OK',
|
||||
'unknown_unencrypted_repo_access_is_ok': 'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK',
|
||||
'use_chunks_archive': 'BORG_USE_CHUNKS_ARCHIVE',
|
||||
}
|
||||
|
||||
DEFAULT_BOOL_OPTION_TO_UPPERCASE_ENVIRONMENT_VARIABLE = {
|
||||
'check_i_know_what_i_am_doing': 'BORG_CHECK_I_KNOW_WHAT_I_AM_DOING',
|
||||
}
|
||||
|
||||
|
||||
def make_environment(config):
|
||||
'''
|
||||
@@ -83,15 +85,15 @@ def make_environment(config):
|
||||
for (
|
||||
option_name,
|
||||
environment_variable_name,
|
||||
) in DEFAULT_BOOL_OPTION_TO_DOWNCASE_ENVIRONMENT_VARIABLE.items():
|
||||
) in DEFAULT_BOOL_OPTION_TO_ENVIRONMENT_VARIABLE.items():
|
||||
if os.environ.get(environment_variable_name) is None:
|
||||
value = config.get(option_name)
|
||||
environment[environment_variable_name] = 'yes' if value else 'no'
|
||||
environment[environment_variable_name] = 'YES' if value else 'NO'
|
||||
|
||||
for (
|
||||
option_name,
|
||||
environment_variable_name,
|
||||
) in DEFAULT_BOOL_OPTION_TO_UPPERCASE_ENVIRONMENT_VARIABLE.items():
|
||||
) in DEFAULT_BOOL_OPTION_TO_UNCONDITIONAL_ENVIRONMENT_VARIABLE.items():
|
||||
value = config.get(option_name)
|
||||
if value is not None:
|
||||
environment[environment_variable_name] = 'YES' if value else 'NO'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import shlex
|
||||
|
||||
import borgmatic.config.paths
|
||||
import borgmatic.logger
|
||||
@@ -90,7 +91,7 @@ def prune_archives(
|
||||
+ (('--list',) if config.get('list_details') else ())
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ (('--dry-run',) if dry_run else ())
|
||||
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
|
||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import logging
|
||||
|
||||
import borgmatic.borg.flags
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def make_rename_command(
|
||||
dry_run,
|
||||
repository_name,
|
||||
old_archive_name,
|
||||
new_archive_name,
|
||||
config,
|
||||
local_borg_version,
|
||||
local_path,
|
||||
remote_path,
|
||||
):
|
||||
return (
|
||||
(local_path, 'rename')
|
||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ borgmatic.borg.flags.make_flags('dry-run', dry_run)
|
||||
+ borgmatic.borg.flags.make_flags('remote-path', remote_path)
|
||||
+ borgmatic.borg.flags.make_flags('umask', config.get('umask'))
|
||||
+ borgmatic.borg.flags.make_flags('log-json', config.get('log_json'))
|
||||
+ borgmatic.borg.flags.make_flags('lock-wait', config.get('lock_wait'))
|
||||
+ borgmatic.borg.flags.make_repository_archive_flags(
|
||||
repository_name, old_archive_name, local_borg_version
|
||||
)
|
||||
+ (new_archive_name,)
|
||||
)
|
||||
|
||||
|
||||
def rename_archive(
|
||||
repository_name,
|
||||
old_archive_name,
|
||||
new_archive_name,
|
||||
dry_run,
|
||||
config,
|
||||
local_borg_version,
|
||||
local_path,
|
||||
remote_path,
|
||||
):
|
||||
command = make_rename_command(
|
||||
dry_run,
|
||||
repository_name,
|
||||
old_archive_name,
|
||||
new_archive_name,
|
||||
config,
|
||||
local_borg_version,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
|
||||
borgmatic.execute.execute_command(
|
||||
command,
|
||||
output_log_level=logging.INFO,
|
||||
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'),
|
||||
)
|
||||
@@ -1,6 +1,7 @@
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import shlex
|
||||
import subprocess
|
||||
|
||||
import borgmatic.config.paths
|
||||
@@ -86,7 +87,7 @@ def create_repository(
|
||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||
+ (('--remote-path', remote_path) if remote_path else ())
|
||||
+ (('--umask', str(umask)) if umask else ())
|
||||
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
|
||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
|
||||
import borgmatic.config.paths
|
||||
@@ -29,6 +30,33 @@ def resolve_archive_name(
|
||||
if archive != 'latest':
|
||||
return archive
|
||||
|
||||
latest_archive = get_latest_archive(
|
||||
repository_path,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
)
|
||||
|
||||
return latest_archive['name']
|
||||
|
||||
|
||||
def get_latest_archive(
|
||||
repository_path,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
consider_checkpoints=False,
|
||||
):
|
||||
'''
|
||||
Returns a dict with information about the latest archive of a repository.
|
||||
|
||||
Raises ValueError if there are no archives in the repository.
|
||||
'''
|
||||
|
||||
full_command = (
|
||||
(
|
||||
local_path,
|
||||
@@ -42,24 +70,28 @@ def resolve_archive_name(
|
||||
+ flags.make_flags('umask', config.get('umask'))
|
||||
+ flags.make_flags('log-json', config.get('log_json'))
|
||||
+ flags.make_flags('lock-wait', config.get('lock_wait'))
|
||||
+ flags.make_flags('consider-checkpoints', consider_checkpoints)
|
||||
+ flags.make_flags('last', 1)
|
||||
+ ('--short',)
|
||||
+ ('--json',)
|
||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||
)
|
||||
|
||||
output = execute_command_and_capture_output(
|
||||
json_output = execute_command_and_capture_output(
|
||||
full_command,
|
||||
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'),
|
||||
)
|
||||
|
||||
archives = json.loads(json_output)['archives']
|
||||
|
||||
try:
|
||||
latest_archive = output.strip().splitlines()[-1]
|
||||
latest_archive = archives[-1]
|
||||
except IndexError:
|
||||
raise ValueError('No archives found in the repository')
|
||||
|
||||
logger.debug(f'Latest archive is {latest_archive}')
|
||||
logger.debug(f'Latest archive is {latest_archive["name"]}')
|
||||
|
||||
return latest_archive
|
||||
|
||||
|
||||
@@ -667,7 +667,7 @@ def make_parsers(schema, unparsed_arguments):
|
||||
dest='make_parent_directories',
|
||||
default=None,
|
||||
action='store_true',
|
||||
help='Create any missing parent directories of the repository directory',
|
||||
help='Create any missing parent directories of the repository directory [Borg 1.x only]',
|
||||
)
|
||||
repo_create_group.add_argument(
|
||||
'-h', '--help', action='help', help='Show this help message and exit'
|
||||
@@ -872,6 +872,11 @@ def make_parsers(schema, unparsed_arguments):
|
||||
create_group.add_argument(
|
||||
'--json', dest='json', default=False, action='store_true', help='Output results as JSON'
|
||||
)
|
||||
create_group.add_argument(
|
||||
'--comment',
|
||||
metavar='COMMENT',
|
||||
help='Add a comment text to the archive',
|
||||
)
|
||||
create_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
|
||||
|
||||
check_parser = action_parsers.add_parser(
|
||||
|
||||
@@ -228,7 +228,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
|
||||
yield from log_error_records(
|
||||
f'{config_filename}: Error getting local Borg version', error
|
||||
)
|
||||
return
|
||||
raise
|
||||
|
||||
for repo in config['repositories']:
|
||||
repo_queue.put(
|
||||
@@ -289,11 +289,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
|
||||
raise encountered_error
|
||||
|
||||
except (OSError, CalledProcessError, ValueError) as error:
|
||||
# No need to repeat logging of the error if it was already logged above.
|
||||
if error_repository:
|
||||
yield from log_error_records('Error running configuration')
|
||||
else:
|
||||
yield from log_error_records('Error running configuration', error)
|
||||
yield from log_error_records('Error running configuration')
|
||||
|
||||
encountered_error = error
|
||||
|
||||
@@ -754,7 +750,7 @@ def log_error_records(
|
||||
except (ValueError, OSError) as error:
|
||||
yield log_record(levelno=levelno, levelname=level_name, msg=str(message))
|
||||
yield log_record(levelno=levelno, levelname=level_name, msg=str(error))
|
||||
except: # noqa: E722
|
||||
except: # noqa: E722, S110
|
||||
# Raising above only as a means of determining the error type. Swallow the exception here
|
||||
# because we don't want the exception to propagate out of this function.
|
||||
pass
|
||||
|
||||
@@ -51,7 +51,7 @@ def schema_to_sample_configuration(schema, source_config=None, level=0, parent_i
|
||||
add_comments_to_configuration_sequence(config, schema, indent=(level * INDENT))
|
||||
elif borgmatic.config.schema.compare_types(schema_type, {'object'}):
|
||||
if source_config and isinstance(source_config, list) and isinstance(source_config[0], dict):
|
||||
source_config = dict(collections.ChainMap(*source_config))
|
||||
source_config = source_config[0]
|
||||
|
||||
config = (
|
||||
ruamel.yaml.comments.CommentedMap(
|
||||
@@ -71,7 +71,7 @@ def schema_to_sample_configuration(schema, source_config=None, level=0, parent_i
|
||||
)
|
||||
indent = (level * INDENT) + (SEQUENCE_INDENT if parent_is_sequence else 0)
|
||||
add_comments_to_configuration_object(
|
||||
config, schema, source_config, indent=indent, skip_first=parent_is_sequence
|
||||
config, schema, source_config, indent=indent, skip_first_field=parent_is_sequence
|
||||
)
|
||||
elif borgmatic.config.schema.compare_types(schema_type, SCALAR_SCHEMA_TYPES, match=all):
|
||||
return example
|
||||
@@ -108,17 +108,30 @@ def comment_out_optional_configuration(rendered_config):
|
||||
'''
|
||||
lines = []
|
||||
optional = False
|
||||
indent_characters = None
|
||||
indent_characters_at_sentinel = None
|
||||
|
||||
for line in rendered_config.split('\n'):
|
||||
indent_characters = len(line) - len(line.lstrip())
|
||||
|
||||
# Upon encountering an optional configuration option, comment out lines until the next blank
|
||||
# line.
|
||||
if line.strip().startswith(f'# {COMMENTED_OUT_SENTINEL}'):
|
||||
optional = True
|
||||
indent_characters_at_sentinel = indent_characters
|
||||
continue
|
||||
|
||||
# Hit a blank line, so reset commenting.
|
||||
if not line.strip():
|
||||
optional = False
|
||||
indent_characters_at_sentinel = None
|
||||
# Dedented, so reset commenting.
|
||||
elif (
|
||||
indent_characters_at_sentinel is not None
|
||||
and indent_characters < indent_characters_at_sentinel
|
||||
):
|
||||
optional = False
|
||||
indent_characters_at_sentinel = None
|
||||
|
||||
lines.append(comment_out_line(line) if optional else line)
|
||||
|
||||
@@ -198,25 +211,28 @@ COMMENTED_OUT_SENTINEL = 'COMMENT_OUT'
|
||||
|
||||
|
||||
def add_comments_to_configuration_object(
|
||||
config, schema, source_config=None, indent=0, skip_first=False
|
||||
config, schema, source_config=None, indent=0, skip_first_field=False
|
||||
):
|
||||
'''
|
||||
Using descriptions from a schema as a source, add those descriptions as comments to the given
|
||||
configuration dict, putting them before each field. Indent the comment the given number of
|
||||
characters.
|
||||
|
||||
If skip_first_field is True, omit the comment for the initial field. This is useful for
|
||||
sequences, where the comment for the first field goes before the sequence itself.
|
||||
|
||||
And a sentinel for commenting out options that are neither in DEFAULT_KEYS nor the the given
|
||||
source configuration dict. The idea is that any options used in the source configuration should
|
||||
stay active in the generated configuration.
|
||||
'''
|
||||
for index, field_name in enumerate(config.keys()):
|
||||
if skip_first and index == 0:
|
||||
if skip_first_field and index == 0:
|
||||
continue
|
||||
|
||||
field_schema = borgmatic.config.schema.get_properties(schema).get(field_name, {})
|
||||
description = field_schema.get('description', '').strip()
|
||||
|
||||
# If this isn't a default key, add an indicator to the comment flagging it to be commented
|
||||
# If this isn't a default key, add an indicator to the comment, flagging it to be commented
|
||||
# out from the sample configuration. This sentinel is consumed by downstream processing that
|
||||
# does the actual commenting out.
|
||||
if field_name not in DEFAULT_KEYS and (
|
||||
@@ -299,6 +315,12 @@ def generate_sample_configuration(
|
||||
source_config = load.load_configuration(source_filename)
|
||||
normalize.normalize(source_filename, source_config)
|
||||
|
||||
# The borgmatic.config.normalize.normalize() function tacks on an empty "bootstrap" if
|
||||
# needed, so the hook gets used by default. But we don't want it to end up in the generated
|
||||
# config unless the user has set it explicitly, as an empty "bootstrap:" won't validate.
|
||||
if source_config and source_config.get('bootstrap') == {}:
|
||||
del source_config['bootstrap']
|
||||
|
||||
destination_config = merge_source_configuration_into_destination(
|
||||
schema_to_sample_configuration(schema, source_config), source_config
|
||||
)
|
||||
|
||||
@@ -159,7 +159,7 @@ def normalize(config_filename, config):
|
||||
dict(
|
||||
levelno=logging.WARNING,
|
||||
levelname='WARNING',
|
||||
msg=f'{config_filename}: The borgmatic_source_directory option is deprecated and will be removed from a future release. Use borgmatic_runtime_directory and borgmatic_state_directory instead.',
|
||||
msg=f'{config_filename}: The borgmatic_source_directory option is deprecated and will be removed from a future release. Use user_runtime_directory and user_state_directory instead.',
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -356,6 +356,8 @@ def normalize(config_filename, config):
|
||||
repository_path.startswith('ssh://')
|
||||
or repository_path.startswith('sftp://')
|
||||
or repository_path.startswith('rclone:')
|
||||
or repository_path.startswith('s3:')
|
||||
or repository_path.startswith('b2:')
|
||||
):
|
||||
config['repositories'].append(repository_dict)
|
||||
else:
|
||||
@@ -365,7 +367,7 @@ def normalize(config_filename, config):
|
||||
dict(
|
||||
levelno=logging.WARNING,
|
||||
levelname='WARNING',
|
||||
msg=f'{config_filename}: Remote repository paths without ssh:// or rclone: syntax are deprecated and support will be removed from a future release. Interpreting "{repository_path}" as "{rewritten_repository_path}"',
|
||||
msg=f'{config_filename}: Remote repository paths without ssh://, sftp://, rclone:, s3:, or b2:, syntax are deprecated and support will be removed from a future release. Interpreting "{repository_path}" as "{rewritten_repository_path}"',
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -103,7 +103,9 @@ class Runtime_directory:
|
||||
|
||||
self.temporary_directory = None
|
||||
else:
|
||||
base_directory = os.environ.get('TMPDIR') or os.environ.get('TEMP') or '/tmp'
|
||||
base_directory = (
|
||||
os.environ.get('TMPDIR') or os.environ.get('TEMP') or '/tmp' # noqa: S108
|
||||
)
|
||||
|
||||
if not base_directory.startswith(os.path.sep):
|
||||
raise ValueError('The temporary directory must be an absolute path')
|
||||
|
||||
@@ -72,7 +72,8 @@ properties:
|
||||
description: |
|
||||
Whether any missing parent directories of the repository
|
||||
path should be created, only used for the repo-create
|
||||
action. Defaults to false.
|
||||
action. Defaults to false. (This option is supported
|
||||
for Borg 1.x only)
|
||||
example: true
|
||||
description: |
|
||||
A required list of local or remote repositories with paths and
|
||||
@@ -490,6 +491,19 @@ properties:
|
||||
Bypass Borg error about a previously unknown unencrypted repository.
|
||||
Defaults to false.
|
||||
example: true
|
||||
debug_passphrase:
|
||||
type: boolean
|
||||
description: |
|
||||
When set true, display debugging information that includes
|
||||
passphrases used and passphrase related environment variables set.
|
||||
Defaults to false.
|
||||
example: true
|
||||
display_passphrase:
|
||||
type: boolean
|
||||
description: |
|
||||
When set true, always shows passphrase and its hex UTF-8 byte
|
||||
sequence. Defaults to false.
|
||||
example: true
|
||||
check_i_know_what_i_am_doing:
|
||||
type: boolean
|
||||
description: |
|
||||
|
||||
@@ -288,6 +288,7 @@ def execute_command(
|
||||
borg_local_path=None,
|
||||
borg_exit_codes=None,
|
||||
run_to_completion=True,
|
||||
close_fds=False, # Necessary for passing credentials via anonymous pipe.
|
||||
):
|
||||
'''
|
||||
Execute the given command (a sequence of command/argument strings) and log its output at the
|
||||
@@ -312,11 +313,10 @@ def execute_command(
|
||||
stdin=input_file,
|
||||
stdout=None if do_not_capture else (output_file or subprocess.PIPE),
|
||||
stderr=None if do_not_capture else (subprocess.PIPE if output_file else subprocess.STDOUT),
|
||||
shell=shell,
|
||||
shell=shell, # noqa: S602
|
||||
env=environment,
|
||||
cwd=working_directory,
|
||||
# Necessary for passing credentials via anonymous pipe.
|
||||
close_fds=False,
|
||||
close_fds=close_fds,
|
||||
)
|
||||
if not run_to_completion:
|
||||
return process
|
||||
@@ -340,6 +340,7 @@ def execute_command_and_capture_output(
|
||||
working_directory=None,
|
||||
borg_local_path=None,
|
||||
borg_exit_codes=None,
|
||||
close_fds=False, # Necessary for passing credentials via anonymous pipe.
|
||||
):
|
||||
'''
|
||||
Execute the given command (a sequence of command/argument strings), capturing and returning its
|
||||
@@ -362,11 +363,10 @@ def execute_command_and_capture_output(
|
||||
command,
|
||||
stdin=input_file,
|
||||
stderr=subprocess.STDOUT if capture_stderr else None,
|
||||
shell=shell,
|
||||
shell=shell, # noqa: S602
|
||||
env=environment,
|
||||
cwd=working_directory,
|
||||
# Necessary for passing credentials via anonymous pipe.
|
||||
close_fds=False,
|
||||
close_fds=close_fds,
|
||||
)
|
||||
except subprocess.CalledProcessError as error:
|
||||
if (
|
||||
@@ -390,6 +390,7 @@ def execute_command_with_processes(
|
||||
working_directory=None,
|
||||
borg_local_path=None,
|
||||
borg_exit_codes=None,
|
||||
close_fds=False, # Necessary for passing credentials via anonymous pipe.
|
||||
):
|
||||
'''
|
||||
Execute the given command (a sequence of command/argument strings) and log its output at the
|
||||
@@ -422,11 +423,10 @@ def execute_command_with_processes(
|
||||
stderr=(
|
||||
None if do_not_capture else (subprocess.PIPE if output_file else subprocess.STDOUT)
|
||||
),
|
||||
shell=shell,
|
||||
shell=shell, # noqa: S602
|
||||
env=environment,
|
||||
cwd=working_directory,
|
||||
# Necessary for passing credentials via anonymous pipe.
|
||||
close_fds=False,
|
||||
close_fds=close_fds,
|
||||
)
|
||||
except (subprocess.CalledProcessError, OSError):
|
||||
# Something has gone wrong. So vent each process' output buffer to prevent it from hanging.
|
||||
|
||||
@@ -144,7 +144,7 @@ def execute_hooks(command_hooks, umask, working_directory, dry_run, **context):
|
||||
output_log_level=(
|
||||
logging.ERROR if hook_config.get('after') == 'error' else logging.ANSWER
|
||||
),
|
||||
shell=True,
|
||||
shell=True, # noqa: S604
|
||||
environment=make_environment(os.environ),
|
||||
working_directory=working_directory,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import collections
|
||||
import glob
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -21,9 +22,44 @@ def use_streaming(hook_config, config): # pragma: no cover
|
||||
return False
|
||||
|
||||
|
||||
def get_subvolume_mount_points(findmnt_command):
|
||||
def get_contained_subvolume_paths(btrfs_command, subvolume_path):
|
||||
'''
|
||||
Given a findmnt command to run, get all sorted Btrfs subvolume mount points.
|
||||
Given the path of a Btrfs subvolume, return it in a sequence along with the paths of its
|
||||
contained subvolumes.
|
||||
|
||||
If the btrfs command errors, log that error and return an empty sequence.
|
||||
'''
|
||||
try:
|
||||
btrfs_output = borgmatic.execute.execute_command_and_capture_output(
|
||||
tuple(btrfs_command.split(' '))
|
||||
+ (
|
||||
'subvolume',
|
||||
'list',
|
||||
subvolume_path,
|
||||
),
|
||||
close_fds=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as error:
|
||||
logger.debug(
|
||||
f'Ignoring Btrfs subvolume {subvolume_path} because of error listing its subvolumes: {error}'
|
||||
)
|
||||
|
||||
return ()
|
||||
|
||||
return (subvolume_path,) + tuple(
|
||||
os.path.join(subvolume_path, line.split(' ')[-1])
|
||||
for line in btrfs_output.splitlines()
|
||||
if line.strip()
|
||||
)
|
||||
|
||||
|
||||
FINDMNT_BTRFS_ROOT_SUBVOLUME_OPTION = 'subvolid=5'
|
||||
|
||||
|
||||
def get_all_subvolume_paths(btrfs_command, findmnt_command):
|
||||
'''
|
||||
Given btrfs and findmnt commands to run, get the sorted paths for all Btrfs subvolumes on the
|
||||
system.
|
||||
'''
|
||||
findmnt_output = borgmatic.execute.execute_command_and_capture_output(
|
||||
tuple(findmnt_command.split(' '))
|
||||
@@ -32,12 +68,28 @@ def get_subvolume_mount_points(findmnt_command):
|
||||
'btrfs',
|
||||
'--json',
|
||||
'--list', # Request a flat list instead of a nested subvolume hierarchy.
|
||||
)
|
||||
),
|
||||
close_fds=True,
|
||||
)
|
||||
|
||||
try:
|
||||
return tuple(
|
||||
sorted(filesystem['target'] for filesystem in json.loads(findmnt_output)['filesystems'])
|
||||
sorted(
|
||||
itertools.chain.from_iterable(
|
||||
# If findmnt gave us a Btrfs root filesystem, list the subvolumes within it.
|
||||
# This is necessary because findmnt only returns a subvolume's mount point
|
||||
# rather than its original subvolume path (which can differ). For instance,
|
||||
# a subvolume might exist at /mnt/subvolume but be mounted at /home/myuser.
|
||||
# findmnt is still useful though because it's a global way to discover all
|
||||
# Btrfs subvolumes—even if we have to do some additional legwork ourselves.
|
||||
(
|
||||
get_contained_subvolume_paths(btrfs_command, filesystem['target'])
|
||||
if FINDMNT_BTRFS_ROOT_SUBVOLUME_OPTION in filesystem['options'].split(',')
|
||||
else (filesystem['target'],)
|
||||
)
|
||||
for filesystem in json.loads(findmnt_output)['filesystems']
|
||||
)
|
||||
)
|
||||
)
|
||||
except json.JSONDecodeError as error:
|
||||
raise ValueError(f'Invalid {findmnt_command} JSON output: {error}')
|
||||
@@ -49,6 +101,12 @@ Subvolume = collections.namedtuple('Subvolume', ('path', 'contained_patterns'),
|
||||
|
||||
|
||||
def get_subvolume_property(btrfs_command, subvolume_path, property_name):
|
||||
'''
|
||||
Given a btrfs command, a subvolume path, and a property name to lookup, return the value of the
|
||||
corresponding property.
|
||||
|
||||
Raise subprocess.CalledProcessError if the btrfs command errors.
|
||||
'''
|
||||
output = borgmatic.execute.execute_command_and_capture_output(
|
||||
tuple(btrfs_command.split(' '))
|
||||
+ (
|
||||
@@ -59,6 +117,7 @@ def get_subvolume_property(btrfs_command, subvolume_path, property_name):
|
||||
subvolume_path,
|
||||
property_name,
|
||||
),
|
||||
close_fds=True,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -72,19 +131,24 @@ def get_subvolume_property(btrfs_command, subvolume_path, property_name):
|
||||
}.get(value, value)
|
||||
|
||||
|
||||
def omit_read_only_subvolume_mount_points(btrfs_command, subvolume_paths):
|
||||
def omit_read_only_subvolume_paths(btrfs_command, subvolume_paths):
|
||||
'''
|
||||
Given a Btrfs command to run and a sequence of Btrfs subvolume mount points, filter them down to
|
||||
just those that are read-write. The idea is that Btrfs can't actually snapshot a read-only
|
||||
subvolume, so we should just ignore them.
|
||||
Given a Btrfs command to run and a sequence of Btrfs subvolume paths, filter them down to just
|
||||
those that are read-write. The idea is that Btrfs can't actually snapshot a read-only subvolume,
|
||||
so we should just ignore them.
|
||||
'''
|
||||
retained_subvolume_paths = []
|
||||
|
||||
for subvolume_path in subvolume_paths:
|
||||
if get_subvolume_property(btrfs_command, subvolume_path, 'ro'):
|
||||
logger.debug(f'Ignoring Btrfs subvolume {subvolume_path} because it is read-only')
|
||||
else:
|
||||
retained_subvolume_paths.append(subvolume_path)
|
||||
try:
|
||||
if get_subvolume_property(btrfs_command, subvolume_path, 'ro'):
|
||||
logger.debug(f'Ignoring Btrfs subvolume {subvolume_path} because it is read-only')
|
||||
else:
|
||||
retained_subvolume_paths.append(subvolume_path)
|
||||
except subprocess.CalledProcessError as error:
|
||||
logger.debug(
|
||||
f'Error determining read-only status of Btrfs subvolume {subvolume_path}: {error}'
|
||||
)
|
||||
|
||||
return tuple(retained_subvolume_paths)
|
||||
|
||||
@@ -92,32 +156,32 @@ def omit_read_only_subvolume_mount_points(btrfs_command, subvolume_paths):
|
||||
def get_subvolumes(btrfs_command, findmnt_command, patterns=None):
|
||||
'''
|
||||
Given a Btrfs command to run and a sequence of configured patterns, find the intersection
|
||||
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.
|
||||
between the current Btrfs filesystem and subvolume paths and the paths of any patterns. The
|
||||
idea is that these pattern paths represent the requested subvolumes to snapshot.
|
||||
|
||||
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.
|
||||
Return the result as a sequence of matching Subvolume instances.
|
||||
'''
|
||||
candidate_patterns = set(patterns or ())
|
||||
subvolumes = []
|
||||
|
||||
# For each subvolume mount point, match it against the given patterns to find the subvolumes to
|
||||
# For each subvolume path, match it against the given patterns to find the subvolumes to
|
||||
# backup. Sort the subvolumes from longest to shortest mount points, so longer mount points get
|
||||
# a whack at the candidate pattern piñata before their parents do. (Patterns are consumed during
|
||||
# this process, so no two subvolumes end up with the same contained patterns.)
|
||||
for mount_point in reversed(
|
||||
omit_read_only_subvolume_mount_points(
|
||||
btrfs_command, get_subvolume_mount_points(findmnt_command)
|
||||
for subvolume_path in reversed(
|
||||
omit_read_only_subvolume_paths(
|
||||
btrfs_command, get_all_subvolume_paths(btrfs_command, findmnt_command)
|
||||
)
|
||||
):
|
||||
subvolumes.extend(
|
||||
Subvolume(mount_point, contained_patterns)
|
||||
Subvolume(subvolume_path, contained_patterns)
|
||||
for contained_patterns in (
|
||||
borgmatic.hooks.data_source.snapshot.get_contained_patterns(
|
||||
mount_point, candidate_patterns
|
||||
subvolume_path, candidate_patterns
|
||||
),
|
||||
)
|
||||
if patterns is None
|
||||
@@ -227,6 +291,7 @@ def snapshot_subvolume(btrfs_command, subvolume_path, snapshot_path): # pragma:
|
||||
snapshot_path,
|
||||
),
|
||||
output_log_level=logging.DEBUG,
|
||||
close_fds=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -241,9 +306,9 @@ def dump_data_sources(
|
||||
'''
|
||||
Given a Btrfs configuration dict, a configuration dict, the borgmatic configuration file paths,
|
||||
the borgmatic runtime directory, the configured patterns, and whether this is a dry run,
|
||||
auto-detect and snapshot any Btrfs subvolume mount points listed in the given patterns. Also
|
||||
update those patterns, replacing subvolume mount points with corresponding snapshot directories
|
||||
so they get stored in the Borg archive instead.
|
||||
auto-detect and snapshot any Btrfs subvolume paths listed in the given patterns. Also update
|
||||
those patterns, replacing subvolume paths with corresponding snapshot directories so they get
|
||||
stored in the Borg archive instead.
|
||||
|
||||
Return an empty sequence, since there are no ongoing dump processes from this hook.
|
||||
|
||||
@@ -298,6 +363,7 @@ def delete_snapshot(btrfs_command, snapshot_path): # pragma: no cover
|
||||
snapshot_path,
|
||||
),
|
||||
output_log_level=logging.DEBUG,
|
||||
close_fds=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -324,8 +390,8 @@ def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, d
|
||||
logger.debug(error)
|
||||
return
|
||||
|
||||
# Reversing the sorted subvolumes ensures that we remove longer mount point paths of child
|
||||
# subvolumes before the shorter mount point paths of parent subvolumes.
|
||||
# Reversing the sorted subvolumes ensures that we remove longer paths of child subvolumes before
|
||||
# the shorter paths of parent subvolumes.
|
||||
for subvolume in reversed(all_subvolumes):
|
||||
subvolume_snapshots_glob = borgmatic.config.paths.replace_temporary_subdirectory_with_glob(
|
||||
os.path.normpath(make_snapshot_path(subvolume.path)),
|
||||
|
||||
@@ -50,7 +50,8 @@ def get_logical_volumes(lsblk_command, patterns=None):
|
||||
'name,path,mountpoint,type',
|
||||
'--json',
|
||||
'--list',
|
||||
)
|
||||
),
|
||||
close_fds=True,
|
||||
)
|
||||
)
|
||||
except json.JSONDecodeError as error:
|
||||
@@ -109,6 +110,7 @@ def snapshot_logical_volume(
|
||||
logical_volume_device,
|
||||
),
|
||||
output_log_level=logging.DEBUG,
|
||||
close_fds=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -129,6 +131,7 @@ def mount_snapshot(mount_command, snapshot_device, snapshot_mount_path): # prag
|
||||
snapshot_mount_path,
|
||||
),
|
||||
output_log_level=logging.DEBUG,
|
||||
close_fds=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -227,12 +230,13 @@ def dump_data_sources(
|
||||
)
|
||||
|
||||
# Get the device path for the snapshot we just created.
|
||||
try:
|
||||
snapshot = get_snapshots(
|
||||
hook_config.get('lvs_command', 'lvs'), snapshot_name=snapshot_name
|
||||
)[0]
|
||||
except IndexError:
|
||||
raise ValueError(f'Cannot find LVM snapshot {snapshot_name}')
|
||||
if not dry_run:
|
||||
try:
|
||||
snapshot = get_snapshots(
|
||||
hook_config.get('lvs_command', 'lvs'), snapshot_name=snapshot_name
|
||||
)[0]
|
||||
except IndexError:
|
||||
raise ValueError(f'Cannot find LVM snapshot {snapshot_name}')
|
||||
|
||||
# Mount the snapshot into a particular named temporary directory so that the snapshot ends
|
||||
# up in the Borg archive at the "original" logical volume mount point path.
|
||||
@@ -277,6 +281,7 @@ def unmount_snapshot(umount_command, snapshot_mount_path): # pragma: no cover
|
||||
borgmatic.execute.execute_command(
|
||||
tuple(umount_command.split(' ')) + (snapshot_mount_path,),
|
||||
output_log_level=logging.DEBUG,
|
||||
close_fds=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -291,6 +296,7 @@ def remove_snapshot(lvremove_command, snapshot_device_path): # pragma: no cover
|
||||
snapshot_device_path,
|
||||
),
|
||||
output_log_level=logging.DEBUG,
|
||||
close_fds=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -318,7 +324,8 @@ def get_snapshots(lvs_command, snapshot_name=None):
|
||||
'lv_name,lv_path',
|
||||
'--select',
|
||||
'lv_attr =~ ^s', # Filter to just snapshots.
|
||||
)
|
||||
),
|
||||
close_fds=True,
|
||||
)
|
||||
)
|
||||
except json.JSONDecodeError as error:
|
||||
|
||||
@@ -74,11 +74,12 @@ def dump_data_sources(
|
||||
|
||||
if dump_format == 'directory':
|
||||
dump.create_parent_directory_for_dump(dump_filename)
|
||||
execute_command(command, shell=True)
|
||||
execute_command(command, shell=True) # noqa: S604
|
||||
else:
|
||||
dump.create_named_pipe_for_dump(dump_filename)
|
||||
processes.append(execute_command(command, shell=True, run_to_completion=False))
|
||||
|
||||
processes.append(
|
||||
execute_command(command, shell=True, run_to_completion=False) # noqa: S604
|
||||
)
|
||||
if not dry_run:
|
||||
patterns.append(
|
||||
borgmatic.borg.pattern.Pattern(
|
||||
|
||||
@@ -224,7 +224,7 @@ def dump_data_sources(
|
||||
dump.create_parent_directory_for_dump(dump_filename)
|
||||
execute_command(
|
||||
command,
|
||||
shell=True,
|
||||
shell=True, # noqa: S604
|
||||
environment=environment,
|
||||
)
|
||||
else:
|
||||
@@ -232,7 +232,7 @@ def dump_data_sources(
|
||||
processes.append(
|
||||
execute_command(
|
||||
command,
|
||||
shell=True,
|
||||
shell=True, # noqa: S604
|
||||
environment=environment,
|
||||
run_to_completion=False,
|
||||
)
|
||||
|
||||
@@ -88,7 +88,9 @@ def dump_data_sources(
|
||||
continue
|
||||
|
||||
dump.create_named_pipe_for_dump(dump_filename)
|
||||
processes.append(execute_command(command, shell=True, run_to_completion=False))
|
||||
processes.append(
|
||||
execute_command(command, shell=True, run_to_completion=False) # noqa: S604
|
||||
)
|
||||
|
||||
if not dry_run:
|
||||
patterns.append(
|
||||
|
||||
@@ -53,7 +53,8 @@ def get_datasets_to_backup(zfs_command, patterns):
|
||||
'filesystem',
|
||||
'-o',
|
||||
f'name,mountpoint,canmount,{BORGMATIC_USER_PROPERTY}',
|
||||
)
|
||||
),
|
||||
close_fds=True,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -131,7 +132,8 @@ def get_all_dataset_mount_points(zfs_command):
|
||||
'filesystem',
|
||||
'-o',
|
||||
'mountpoint',
|
||||
)
|
||||
),
|
||||
close_fds=True,
|
||||
)
|
||||
|
||||
return tuple(
|
||||
@@ -158,6 +160,7 @@ def snapshot_dataset(zfs_command, full_snapshot_name): # pragma: no cover
|
||||
full_snapshot_name,
|
||||
),
|
||||
output_log_level=logging.DEBUG,
|
||||
close_fds=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -180,6 +183,7 @@ def mount_snapshot(mount_command, full_snapshot_name, snapshot_mount_path): # p
|
||||
snapshot_mount_path,
|
||||
),
|
||||
output_log_level=logging.DEBUG,
|
||||
close_fds=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -310,6 +314,7 @@ def unmount_snapshot(umount_command, snapshot_mount_path): # pragma: no cover
|
||||
borgmatic.execute.execute_command(
|
||||
tuple(umount_command.split(' ')) + (snapshot_mount_path,),
|
||||
output_log_level=logging.DEBUG,
|
||||
close_fds=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -325,6 +330,7 @@ def destroy_snapshot(zfs_command, full_snapshot_name): # pragma: no cover
|
||||
full_snapshot_name,
|
||||
),
|
||||
output_log_level=logging.DEBUG,
|
||||
close_fds=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -342,7 +348,8 @@ def get_all_snapshots(zfs_command):
|
||||
'snapshot',
|
||||
'-o',
|
||||
'name',
|
||||
)
|
||||
),
|
||||
close_fds=True,
|
||||
)
|
||||
|
||||
return tuple(line.rstrip() for line in list_output.splitlines())
|
||||
|
||||
@@ -11,6 +11,7 @@ MONITOR_STATE_TO_CRONHUB = {
|
||||
monitor.State.FINISH: 'finish',
|
||||
monitor.State.FAIL: 'fail',
|
||||
}
|
||||
TIMEOUT_SECONDS = 10
|
||||
|
||||
|
||||
def initialize_monitor(
|
||||
@@ -45,7 +46,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
||||
if not dry_run:
|
||||
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||
try:
|
||||
response = requests.get(ping_url)
|
||||
response = requests.get(ping_url, timeout=TIMEOUT_SECONDS)
|
||||
if not response.ok:
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.RequestException as error:
|
||||
|
||||
@@ -11,6 +11,7 @@ MONITOR_STATE_TO_CRONITOR = {
|
||||
monitor.State.FINISH: 'complete',
|
||||
monitor.State.FAIL: 'fail',
|
||||
}
|
||||
TIMEOUT_SECONDS = 10
|
||||
|
||||
|
||||
def initialize_monitor(
|
||||
@@ -40,7 +41,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
||||
if not dry_run:
|
||||
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||
try:
|
||||
response = requests.get(ping_url)
|
||||
response = requests.get(ping_url, timeout=TIMEOUT_SECONDS)
|
||||
if not response.ok:
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.RequestException as error:
|
||||
|
||||
@@ -17,6 +17,7 @@ MONITOR_STATE_TO_HEALTHCHECKS = {
|
||||
|
||||
DEFAULT_PING_BODY_LIMIT_BYTES = 100000
|
||||
HANDLER_IDENTIFIER = 'healthchecks'
|
||||
TIMEOUT_SECONDS = 10
|
||||
|
||||
|
||||
def initialize_monitor(hook_config, config, config_filename, monitoring_log_level, dry_run):
|
||||
@@ -84,7 +85,10 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
||||
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||
try:
|
||||
response = requests.post(
|
||||
ping_url, data=payload.encode('utf-8'), verify=hook_config.get('verify_tls', True)
|
||||
ping_url,
|
||||
data=payload.encode('utf-8'),
|
||||
verify=hook_config.get('verify_tls', True),
|
||||
timeout=TIMEOUT_SECONDS,
|
||||
)
|
||||
if not response.ok:
|
||||
response.raise_for_status()
|
||||
|
||||
@@ -15,6 +15,7 @@ MONITOR_STATE_TO_LOKI = {
|
||||
monitor.State.FINISH: 'Finished',
|
||||
monitor.State.FAIL: 'Failed',
|
||||
}
|
||||
TIMEOUT_SECONDS = 10
|
||||
|
||||
# Threshold at which logs get flushed to loki
|
||||
MAX_BUFFER_LINES = 100
|
||||
@@ -69,7 +70,9 @@ class Loki_log_buffer:
|
||||
request_header = {'Content-Type': 'application/json'}
|
||||
|
||||
try:
|
||||
result = requests.post(self.url, headers=request_header, data=request_body, timeout=5)
|
||||
result = requests.post(
|
||||
self.url, headers=request_header, data=request_body, timeout=TIMEOUT_SECONDS
|
||||
)
|
||||
result.raise_for_status()
|
||||
except requests.RequestException:
|
||||
logger.warning('Failed to upload logs to loki')
|
||||
|
||||
@@ -7,6 +7,9 @@ import borgmatic.hooks.credential.parse
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
TIMEOUT_SECONDS = 10
|
||||
|
||||
|
||||
def initialize_monitor(
|
||||
ping_url, config, config_filename, monitoring_log_level, dry_run
|
||||
): # pragma: no cover
|
||||
@@ -82,7 +85,9 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
||||
if not dry_run:
|
||||
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||
try:
|
||||
response = requests.post(f'{base_url}/{topic}', headers=headers, auth=auth)
|
||||
response = requests.post(
|
||||
f'{base_url}/{topic}', headers=headers, auth=auth, timeout=TIMEOUT_SECONDS
|
||||
)
|
||||
if not response.ok:
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.RequestException as error:
|
||||
|
||||
@@ -14,6 +14,7 @@ logger = logging.getLogger(__name__)
|
||||
EVENTS_API_URL = 'https://events.pagerduty.com/v2/enqueue'
|
||||
DEFAULT_LOGS_PAYLOAD_LIMIT_BYTES = 10000
|
||||
HANDLER_IDENTIFIER = 'pagerduty'
|
||||
TIMEOUT_SECONDS = 10
|
||||
|
||||
|
||||
def initialize_monitor(hook_config, config, config_filename, monitoring_log_level, dry_run):
|
||||
@@ -94,7 +95,9 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
||||
|
||||
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||
try:
|
||||
response = requests.post(EVENTS_API_URL, data=payload.encode('utf-8'))
|
||||
response = requests.post(
|
||||
EVENTS_API_URL, data=payload.encode('utf-8'), timeout=TIMEOUT_SECONDS
|
||||
)
|
||||
if not response.ok:
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.RequestException as error:
|
||||
|
||||
@@ -8,6 +8,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
EMERGENCY_PRIORITY = 2
|
||||
TIMEOUT_SECONDS = 10
|
||||
|
||||
|
||||
def initialize_monitor(
|
||||
@@ -79,6 +80,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
||||
'https://api.pushover.net/1/messages.json',
|
||||
headers={'Content-type': 'application/x-www-form-urlencoded'},
|
||||
data=data,
|
||||
timeout=TIMEOUT_SECONDS,
|
||||
)
|
||||
if not response.ok:
|
||||
response.raise_for_status()
|
||||
|
||||
@@ -6,6 +6,9 @@ import requests
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
TIMEOUT_SECONDS = 10
|
||||
|
||||
|
||||
def initialize_monitor(
|
||||
ping_url, config, config_filename, monitoring_log_level, dry_run
|
||||
): # pragma: no cover
|
||||
@@ -61,7 +64,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
||||
|
||||
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||
try:
|
||||
response = requests.post(f'{cron_url}?status={status}')
|
||||
response = requests.post(f'{cron_url}?status={status}', timeout=TIMEOUT_SECONDS)
|
||||
if not response.ok:
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.RequestException as error:
|
||||
|
||||
@@ -5,6 +5,9 @@ import requests
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
TIMEOUT_SECONDS = 10
|
||||
|
||||
|
||||
def initialize_monitor(
|
||||
push_url, config, config_filename, monitoring_log_level, dry_run
|
||||
): # pragma: no cover
|
||||
@@ -37,7 +40,11 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
||||
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||
|
||||
try:
|
||||
response = requests.get(f'{push_url}?{query}', verify=hook_config.get('verify_tls', True))
|
||||
response = requests.get(
|
||||
f'{push_url}?{query}',
|
||||
verify=hook_config.get('verify_tls', True),
|
||||
timeout=TIMEOUT_SECONDS,
|
||||
)
|
||||
if not response.ok:
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.RequestException as error:
|
||||
|
||||
@@ -7,6 +7,9 @@ import borgmatic.hooks.credential.parse
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
TIMEOUT_SECONDS = 10
|
||||
|
||||
|
||||
def initialize_monitor(
|
||||
ping_url, config, config_filename, monitoring_log_level, dry_run
|
||||
): # pragma: no cover
|
||||
@@ -28,7 +31,7 @@ def send_zabbix_request(server, headers, data):
|
||||
logger.debug(f'Sending a "{data["method"]}" request to the Zabbix server')
|
||||
|
||||
try:
|
||||
response = requests.post(server, headers=headers, json=data)
|
||||
response = requests.post(server, headers=headers, json=data, timeout=TIMEOUT_SECONDS)
|
||||
|
||||
if not response.ok:
|
||||
response.raise_for_status()
|
||||
|
||||
@@ -95,6 +95,10 @@ configured, meaning that the hook commands are run in that directory.
|
||||
same `everything` command hook is present in multiple configuration files,
|
||||
borgmatic only runs it once.
|
||||
|
||||
borgmatic's `--repository` flag does not impact which command hooks get run. But
|
||||
you can use the `--config` flag to limit the configuration files (and thus
|
||||
command hooks) used.
|
||||
|
||||
|
||||
### Order of execution
|
||||
|
||||
@@ -130,6 +134,10 @@ have a chance to run. Whereas the `after: error` hook doesn't run until all
|
||||
actions for—and repositories in—a configuration file have had a chance to
|
||||
execute.
|
||||
|
||||
And if there are multiple hooks defined for a particular step (e.g. `before:
|
||||
action` for `create`), then those hooks are run in the order they're defined in
|
||||
configuration.
|
||||
|
||||
|
||||
### Deprecated command hooks
|
||||
|
||||
|
||||
@@ -12,52 +12,18 @@ numeric ID indicating success, warning, or error. borgmatic consumes this exit
|
||||
code to decide how to respond. Normally, a Borg error results in a borgmatic
|
||||
error, while a Borg warning or success doesn't.
|
||||
|
||||
But if that default behavior isn't sufficient for your needs, you can
|
||||
customize how borgmatic interprets [Borg's exit
|
||||
<span class="minilink minilink-addedin">With Borg version 1.4+</span> If the
|
||||
default behavior isn't sufficient for your needs, you can customize how
|
||||
borgmatic interprets [Borg's exit
|
||||
codes](https://borgbackup.readthedocs.io/en/stable/usage/general.html#return-codes).
|
||||
For instance, to elevate Borg warnings to errors, thereby causing borgmatic to
|
||||
error on them, use the following borgmatic configuration:
|
||||
|
||||
```yaml
|
||||
borg_exit_codes:
|
||||
- exit_code: 1
|
||||
treat_as: error
|
||||
```
|
||||
|
||||
Be aware though that Borg exits with a warning code for a variety of benign
|
||||
situations such as files changing while they're being read, so this example
|
||||
may not meet your needs. Keep reading though for more granular exit code
|
||||
configuration.
|
||||
|
||||
Here's an example that squashes Borg errors to warnings:
|
||||
|
||||
```yaml
|
||||
borg_exit_codes:
|
||||
- exit_code: 2
|
||||
treat_as: warning
|
||||
```
|
||||
|
||||
Be careful with this example though, because it prevents borgmatic from
|
||||
erroring when Borg errors, which may not be desirable.
|
||||
|
||||
|
||||
### More granular configuration
|
||||
|
||||
<span class="minilink minilink-addedin">New in Borg version 1.4</span> Borg
|
||||
support for [more granular exit
|
||||
codes](https://borgbackup.readthedocs.io/en/1.4-maint/usage/general.html#return-codes)
|
||||
means that you can configure borgmatic to respond to specific Borg conditions.
|
||||
See the full list of [Borg 1.4 error and warning exit
|
||||
codes](https://borgbackup.readthedocs.io/en/stable/internals/frontends.html#message-ids).
|
||||
The `rc:` numeric value there tells you the exit code for each.
|
||||
|
||||
For instance, this borgmatic configuration elevates all Borg backup file
|
||||
permission warnings (exit code `105`)—and only those warnings—to errors:
|
||||
|
||||
```yaml
|
||||
borg_exit_codes:
|
||||
- exit_code: 105
|
||||
treat_as: error
|
||||
- code: 105
|
||||
treat_as: error
|
||||
```
|
||||
|
||||
The following configuration does that *and* elevates backup file not found
|
||||
@@ -65,16 +31,20 @@ warnings (exit code `107`) to errors as well:
|
||||
|
||||
```yaml
|
||||
borg_exit_codes:
|
||||
- exit_code: 105
|
||||
treat_as: error
|
||||
- exit_code: 107
|
||||
treat_as: error
|
||||
- code: 105
|
||||
treat_as: error
|
||||
- code: 107
|
||||
treat_as: error
|
||||
```
|
||||
|
||||
See the full list of [Borg 1.4 error and warning exit
|
||||
codes](https://borgbackup.readthedocs.io/en/stable/internals/frontends.html#message-ids).
|
||||
The `rc:` numeric value there tells you the exit code for each.
|
||||
|
||||
If you don't know the exit code for a particular Borg error or warning you're
|
||||
experiencing, you can usually find it in your borgmatic output when
|
||||
`--verbosity 2` is enabled. For instance, here's a snippet of that output when
|
||||
a backup file is not found:
|
||||
experiencing, you can usually find it in your borgmatic output when `--verbosity
|
||||
2` is enabled. For instance, here's a snippet of that output when a backup file
|
||||
is not found:
|
||||
|
||||
```
|
||||
/noexist: stat: [Errno 2] No such file or directory: '/noexist'
|
||||
@@ -84,3 +54,30 @@ terminating with warning status, rc 107
|
||||
|
||||
So if you want to configure borgmatic to treat this as an error instead of a
|
||||
warning, the exit status to use is `107`.
|
||||
|
||||
<span class="minilink minilink-addedin">With Borg version 1.2 and earlier</span>
|
||||
Older versions of Borg didn't support granular exit codes, but still
|
||||
distinguished between Borg errors and warnings. For instance, to elevate Borg
|
||||
warnings to errors, thereby causing borgmatic to error on them, use the
|
||||
following borgmatic configuration with Borg 1.2 or below:
|
||||
|
||||
```yaml
|
||||
borg_exit_codes:
|
||||
- code: 1
|
||||
treat_as: error
|
||||
```
|
||||
|
||||
Be aware though that Borg exits with a warning code for a variety of benign
|
||||
situations such as files changing while they're being read, so this example
|
||||
may not meet your needs.
|
||||
|
||||
Here's another Borg 1.2 example that squashes Borg errors to warnings:
|
||||
|
||||
```yaml
|
||||
borg_exit_codes:
|
||||
- code: 2
|
||||
treat_as: warning
|
||||
```
|
||||
|
||||
Be careful with this example though, because it prevents borgmatic from
|
||||
erroring when Borg errors, which may not be desirable.
|
||||
|
||||
@@ -141,7 +141,7 @@ for more information.
|
||||
|
||||
<span class="minilink minilink-addedin">With Borg version 2.x</span>See the
|
||||
[match archives
|
||||
documentation](https://borgbackup.readthedocs.io/en/2.0.0b13/usage/help.html#borg-help-match-archives).
|
||||
documentation](https://borgbackup.readthedocs.io/en/2.0.0b16/usage/help.html#borg-help-match-archives).
|
||||
|
||||
Some borgmatic command-line actions also have a `--match-archives` flag that
|
||||
overrides both the auto-matching behavior and the `match_archives`
|
||||
|
||||
@@ -144,9 +144,8 @@ class="minilink minilink-addedin">Beta feature</span> borgmatic supports taking
|
||||
snapshots with the [Btrfs filesystem](https://btrfs.readthedocs.io/) and sending
|
||||
those snapshots to Borg for backup.
|
||||
|
||||
To use this feature, first you need one or more Btrfs subvolumes on mounted
|
||||
filesystems. Then, enable Btrfs within borgmatic by adding the following line to
|
||||
your configuration file:
|
||||
To use this feature, first you need one or more Btrfs subvolumes. Then, enable
|
||||
Btrfs within borgmatic by adding the following line to your configuration file:
|
||||
|
||||
```yaml
|
||||
btrfs:
|
||||
@@ -169,13 +168,18 @@ feedback](https://torsion.org/borgmatic/#issues) you have on this feature.
|
||||
|
||||
#### Subvolume discovery
|
||||
|
||||
For any read-write subvolume you'd like backed up, add its mount point path to
|
||||
For any read-write subvolume you'd like backed up, add its subvolume path to
|
||||
borgmatic's `source_directories` option. Btrfs does not support snapshotting
|
||||
read-only subvolumes.
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.9.6</span> Or include
|
||||
the mount point as a root pattern with borgmatic's `patterns` or `patterns_from`
|
||||
options.
|
||||
<span class="minilink minilink-addedin">New in version 2.0.7</span> The path can
|
||||
be either the path of the subvolume itself or the mount point where the
|
||||
subvolume is mounted. Prior to version 2.0.7, the subvolume path could not be
|
||||
used if the subvolume was mounted elsewhere; only the mount point could be used.
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.9.6</span> Instead of
|
||||
using `source_directories`, you can include the subvolume path 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
|
||||
@@ -184,25 +188,24 @@ responsible for cleaning up (deleting) these snapshots after a backup completes.
|
||||
borgmatic is smart enough to look at the parent (and grandparent, etc.)
|
||||
directories of each of your `source_directories` to discover any subvolumes. For
|
||||
instance, let's say you add `/var/log` and `/var/lib` to your source
|
||||
directories, but `/var` is a subvolume mount point. borgmatic will discover that
|
||||
and snapshot `/var` accordingly. This also works even with nested subvolumes;
|
||||
directories, but `/var` is a subvolume path. borgmatic will discover that and
|
||||
snapshot `/var` accordingly. This also works even with nested subvolumes;
|
||||
borgmatic selects the subvolume that's the "closest" parent to your source
|
||||
directories.
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.9.6</span> When using
|
||||
[patterns](https://borgbackup.readthedocs.io/en/stable/usage/help.html#borg-help-patterns),
|
||||
the initial portion of a pattern's path that you intend borgmatic to match
|
||||
against a subvolume mount point can't have globs or other non-literal characters
|
||||
in it—or it won't actually match. For instance, a subvolume mount point of
|
||||
`/var` would match a pattern of `+ fm:/var/*/data`, but borgmatic isn't
|
||||
currently smart enough to match `/var` to a pattern like `+ fm:/v*/lib/data`.
|
||||
against a subvolume path can't have globs or other non-literal characters in
|
||||
it—or it won't actually match. For instance, a subvolume path of `/var` would
|
||||
match a pattern of `+ fm:/var/*/data`, but borgmatic isn't currently smart
|
||||
enough to match `/var` to a pattern like `+ fm:/v*/lib/data`.
|
||||
|
||||
Additionally, borgmatic rewrites the snapshot file paths so that they appear at
|
||||
their original subvolume locations in a Borg archive. For instance, if your
|
||||
subvolume is mounted at `/var/subvolume`, then the snapshotted files will appear
|
||||
in an archive at `/var/subvolume` as well—even if borgmatic has to mount the
|
||||
snapshot somewhere in `/var/subvolume/.borgmatic-snapshot-1234/` to perform the
|
||||
backup.
|
||||
subvolume path is `/var/subvolume`, then the snapshotted files will appear in an
|
||||
archive at `/var/subvolume` as well—even if borgmatic has to mount the snapshot
|
||||
somewhere in `/var/subvolume/.borgmatic-snapshot-1234/` to perform the backup.
|
||||
|
||||
<span class="minilink minilink-addedin">With Borg version 1.2 and
|
||||
earlier</span>Snapshotted files are instead stored at a path dependent on the
|
||||
|
||||
@@ -193,7 +193,7 @@ confirmation of success—or tells you if something hasn't been transferred yet.
|
||||
Note that by omitting the `--upgrader` flag, you can also do archive transfers
|
||||
between related Borg 2 repositories without upgrading, even down to individual
|
||||
archives. For more on that functionality, see the [Borg transfer
|
||||
documentation](https://borgbackup.readthedocs.io/en/2.0.0b13/usage/transfer.html).
|
||||
documentation](https://borgbackup.readthedocs.io/en/2.0.0b16/usage/transfer.html).
|
||||
|
||||
That's it! Now you can use your new Borg 2 repository as normal with
|
||||
borgmatic. If you've got multiple repositories, repeat the above process for
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "borgmatic"
|
||||
version = "2.0.5"
|
||||
version = "2.0.7"
|
||||
authors = [
|
||||
{ name="Dan Helfman", email="witten@torsion.org" },
|
||||
]
|
||||
|
||||
+22
-19
@@ -1,32 +1,35 @@
|
||||
appdirs==1.4.4
|
||||
apprise==1.8.0
|
||||
attrs==23.2.0
|
||||
black==24.4.2
|
||||
certifi==2024.7.4
|
||||
apprise==1.9.3
|
||||
attrs==25.3.0
|
||||
bandit==1.8.5
|
||||
black==25.1.0
|
||||
certifi==2025.6.15
|
||||
chardet==5.2.0
|
||||
click==8.1.7
|
||||
codespell==2.2.6
|
||||
coverage==7.5.1
|
||||
flake8==7.0.0
|
||||
click==8.2.1
|
||||
codespell==2.4.1
|
||||
coverage==7.9.1
|
||||
flake8==7.3.0
|
||||
flake8-bandit==4.1.1
|
||||
flake8-quotes==3.4.0
|
||||
flake8-use-fstring==1.4
|
||||
flake8-variables-names==0.0.6
|
||||
flexmock==0.12.1
|
||||
idna==3.7
|
||||
isort==5.13.2
|
||||
jsonschema==4.22.0
|
||||
Markdown==3.6
|
||||
idna==3.10
|
||||
isort==6.0.1
|
||||
jsonschema==4.24.0
|
||||
Markdown==3.8.2
|
||||
mccabe==0.7.0
|
||||
packaging==24.0
|
||||
packaging==25.0
|
||||
pathspec==0.12.1
|
||||
pluggy==1.5.0
|
||||
pbr==6.1.1
|
||||
pluggy==1.6.0
|
||||
py==1.11.0
|
||||
pycodestyle==2.11.1
|
||||
pyflakes==3.2.0
|
||||
pytest==8.2.1
|
||||
pytest-cov==5.0.0
|
||||
pycodestyle==2.14.0
|
||||
pyflakes==3.4.0
|
||||
pytest==8.4.1
|
||||
pytest-cov==6.2.1
|
||||
PyYAML>5.0.0
|
||||
regex
|
||||
requests==2.32.2
|
||||
requests==2.32.4
|
||||
ruamel.yaml>0.15.0
|
||||
toml==0.10.2
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import logging
|
||||
|
||||
from borgmatic.borg import rename as module
|
||||
from tests.unit.test_verbosity import insert_logging_mock
|
||||
|
||||
|
||||
def test_make_rename_command_includes_log_info():
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
command = module.make_rename_command(
|
||||
dry_run=False,
|
||||
repository_name='repo',
|
||||
old_archive_name='old_archive',
|
||||
new_archive_name='new_archive',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert command == ('borg', 'rename', '--info', 'repo::old_archive', 'new_archive')
|
||||
|
||||
|
||||
def test_make_rename_command_includes_log_debug():
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
command = module.make_rename_command(
|
||||
dry_run=False,
|
||||
repository_name='repo',
|
||||
old_archive_name='old_archive',
|
||||
new_archive_name='new_archive',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert command == ('borg', 'rename', '--debug', '--show-rc', 'repo::old_archive', 'new_archive')
|
||||
|
||||
|
||||
def test_make_rename_command_includes_dry_run():
|
||||
command = module.make_rename_command(
|
||||
dry_run=True,
|
||||
repository_name='repo',
|
||||
old_archive_name='old_archive',
|
||||
new_archive_name='new_archive',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert command == ('borg', 'rename', '--dry-run', 'repo::old_archive', 'new_archive')
|
||||
|
||||
|
||||
def test_make_rename_command_includes_remote_path():
|
||||
command = module.make_rename_command(
|
||||
dry_run=False,
|
||||
repository_name='repo',
|
||||
old_archive_name='old_archive',
|
||||
new_archive_name='new_archive',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
local_path='borg',
|
||||
remote_path='borg1',
|
||||
)
|
||||
|
||||
assert command == (
|
||||
'borg',
|
||||
'rename',
|
||||
'--remote-path',
|
||||
'borg1',
|
||||
'repo::old_archive',
|
||||
'new_archive',
|
||||
)
|
||||
|
||||
|
||||
def test_make_rename_command_includes_umask():
|
||||
command = module.make_rename_command(
|
||||
dry_run=False,
|
||||
repository_name='repo',
|
||||
old_archive_name='old_archive',
|
||||
new_archive_name='new_archive',
|
||||
config={'umask': '077'},
|
||||
local_borg_version='1.2.3',
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert command == ('borg', 'rename', '--umask', '077', 'repo::old_archive', 'new_archive')
|
||||
|
||||
|
||||
def test_make_rename_command_includes_log_json():
|
||||
command = module.make_rename_command(
|
||||
dry_run=False,
|
||||
repository_name='repo',
|
||||
old_archive_name='old_archive',
|
||||
new_archive_name='new_archive',
|
||||
config={'log_json': True},
|
||||
local_borg_version='1.2.3',
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert command == ('borg', 'rename', '--log-json', 'repo::old_archive', 'new_archive')
|
||||
|
||||
|
||||
def test_make_rename_command_includes_lock_wait():
|
||||
command = module.make_rename_command(
|
||||
dry_run=False,
|
||||
repository_name='repo',
|
||||
old_archive_name='old_archive',
|
||||
new_archive_name='new_archive',
|
||||
config={'lock_wait': 5},
|
||||
local_borg_version='1.2.3',
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert command == ('borg', 'rename', '--lock-wait', '5', 'repo::old_archive', 'new_archive')
|
||||
@@ -2,6 +2,7 @@ import subprocess
|
||||
|
||||
from flexmock import flexmock
|
||||
|
||||
import borgmatic.hooks.command
|
||||
from borgmatic.commands import borgmatic as module
|
||||
|
||||
|
||||
@@ -42,6 +43,28 @@ def test_run_configuration_without_error_pings_monitoring_hooks_start_and_finish
|
||||
list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
|
||||
|
||||
|
||||
def test_run_configuration_with_borg_version_error_pings_after_command_hook_with_fail_state():
|
||||
config = {
|
||||
'repositories': [{'path': 'foo'}],
|
||||
'commands': ({'after': 'configuration', 'run': ['echo after'], 'states': ['fail']},),
|
||||
}
|
||||
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
|
||||
flexmock(module.borg_version).should_receive('local_borg_version').and_raise(ValueError)
|
||||
flexmock(module).should_receive('run_actions').and_return([])
|
||||
flexmock(module.dispatch).should_receive('call_hooks')
|
||||
flexmock(borgmatic.hooks.command).should_receive('execute_hooks')
|
||||
flexmock(borgmatic.hooks.command).should_receive('execute_hooks').with_args(
|
||||
config['commands'],
|
||||
umask=object,
|
||||
working_directory=object,
|
||||
dry_run=False,
|
||||
log_file=object,
|
||||
configuration_filename=object,
|
||||
).once()
|
||||
|
||||
list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
|
||||
|
||||
|
||||
def test_run_configuration_with_action_error_pings_monioring_hooks_start_and_fail():
|
||||
config = {'repositories': [{'path': 'foo'}]}
|
||||
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
|
||||
|
||||
@@ -126,6 +126,45 @@ def test_schema_to_sample_configuration_comments_out_non_source_config_options_i
|
||||
assert 'field3' not in config[0].ca.items
|
||||
|
||||
|
||||
def test_schema_to_sample_configuration_comments_out_non_source_config_options_in_sequence_of_maps_with_different_subschemas():
|
||||
schema = {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'oneOf': [
|
||||
{
|
||||
'properties': dict(
|
||||
[
|
||||
('field1', {'type': 'string', 'example': 'Example 1'}),
|
||||
('field2', {'type': 'string', 'example': 'Example 2'}),
|
||||
]
|
||||
)
|
||||
},
|
||||
{
|
||||
'properties': dict(
|
||||
[
|
||||
('field2', {'type': 'string', 'example': 'Example 2'}),
|
||||
('field3', {'type': 'string', 'example': 'Example 3'}),
|
||||
]
|
||||
)
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
source_config = [{'field1': 'value'}, {'field3': 'value'}]
|
||||
|
||||
config = module.schema_to_sample_configuration(schema, source_config)
|
||||
|
||||
assert config == [
|
||||
dict([('field1', 'Example 1'), ('field2', 'Example 2'), ('field3', 'Example 3')])
|
||||
]
|
||||
|
||||
# The first field in a sequence does not get commented.
|
||||
assert 'field1' not in config[0].ca.items
|
||||
assert 'COMMENT_OUT' in config[0].ca.items['field2'][1][-1]._value
|
||||
assert 'COMMENT_OUT' in config[0].ca.items['field3'][1][-1]._value
|
||||
|
||||
|
||||
def test_comment_out_line_skips_blank_line():
|
||||
line = ' \n'
|
||||
|
||||
@@ -169,8 +208,11 @@ foo:
|
||||
- quux
|
||||
|
||||
repositories:
|
||||
- one
|
||||
- two
|
||||
- path: foo
|
||||
# COMMENT_OUT
|
||||
label: bar
|
||||
- path: baz
|
||||
label: quux
|
||||
|
||||
# This comment should be kept.
|
||||
# COMMENT_OUT
|
||||
@@ -185,8 +227,10 @@ other: thing
|
||||
# - quux
|
||||
|
||||
repositories:
|
||||
- one
|
||||
- two
|
||||
- path: foo
|
||||
# label: bar
|
||||
- path: baz
|
||||
label: quux
|
||||
|
||||
# This comment should be kept.
|
||||
# other: thing
|
||||
@@ -297,14 +341,28 @@ def test_add_comments_to_configuration_comments_out_non_source_config_options():
|
||||
assert 'baz' not in config.ca.items
|
||||
|
||||
|
||||
def test_add_comments_to_configuration_object_with_skip_first_does_not_comment_out_first_option():
|
||||
def test_add_comments_to_configuration_object_with_skip_first_field_does_not_comment_out_first_option():
|
||||
config = module.ruamel.yaml.comments.CommentedMap([('foo', 33), ('bar', 44), ('baz', 55)])
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'properties': {'foo': {'description': 'Foo'}, 'bar': {'description': 'Bar'}},
|
||||
}
|
||||
|
||||
module.add_comments_to_configuration_object(config, schema, skip_first=True)
|
||||
module.add_comments_to_configuration_object(config, schema, skip_first_field=True)
|
||||
|
||||
assert 'foo' not in config.ca.items
|
||||
assert 'COMMENT_OUT' in config.ca.items['bar'][1][-1]._value
|
||||
assert 'baz' not in config.ca.items
|
||||
|
||||
|
||||
def test_add_comments_to_configuration_object_with_skip_first_field_does_not_comment_out_first_option():
|
||||
config = module.ruamel.yaml.comments.CommentedMap([('foo', 33), ('bar', 44), ('baz', 55)])
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'properties': {'foo': {'description': 'Foo'}, 'bar': {'description': 'Bar'}},
|
||||
}
|
||||
|
||||
module.add_comments_to_configuration_object(config, schema, skip_first_field=True)
|
||||
|
||||
assert 'foo' not in config.ca.items
|
||||
assert 'COMMENT_OUT' in config.ca.items['bar'][1][-1]._value
|
||||
@@ -326,15 +384,39 @@ def test_generate_sample_configuration_does_not_raise():
|
||||
module.generate_sample_configuration(False, None, 'dest.yaml', 'schema.yaml')
|
||||
|
||||
|
||||
def test_generate_sample_configuration_with_source_filename_does_not_raise():
|
||||
def test_generate_sample_configuration_with_source_filename_omits_empty_bootstrap_field():
|
||||
builtins = flexmock(sys.modules['builtins'])
|
||||
builtins.should_receive('open').with_args('schema.yaml').and_return('')
|
||||
flexmock(module.ruamel.yaml).should_receive('YAML').and_return(
|
||||
flexmock(load=lambda filename: {})
|
||||
)
|
||||
flexmock(module.load).should_receive('load_configuration')
|
||||
flexmock(module.load).should_receive('load_configuration').and_return(
|
||||
{'bootstrap': {}, 'foo': 'bar'}
|
||||
)
|
||||
flexmock(module.normalize).should_receive('normalize')
|
||||
flexmock(module).should_receive('schema_to_sample_configuration')
|
||||
flexmock(module).should_receive('schema_to_sample_configuration').with_args(
|
||||
object, {'foo': 'bar'}
|
||||
).once()
|
||||
flexmock(module).should_receive('merge_source_configuration_into_destination')
|
||||
flexmock(module).should_receive('render_configuration')
|
||||
flexmock(module).should_receive('comment_out_optional_configuration')
|
||||
flexmock(module).should_receive('write_configuration')
|
||||
|
||||
module.generate_sample_configuration(False, 'source.yaml', 'dest.yaml', 'schema.yaml')
|
||||
|
||||
|
||||
def test_generate_sample_configuration_with_source_filename_keeps_non_empty_bootstrap_field():
|
||||
builtins = flexmock(sys.modules['builtins'])
|
||||
builtins.should_receive('open').with_args('schema.yaml').and_return('')
|
||||
flexmock(module.ruamel.yaml).should_receive('YAML').and_return(
|
||||
flexmock(load=lambda filename: {})
|
||||
)
|
||||
source_config = {'bootstrap': {'stuff': 'here'}, 'foo': 'bar'}
|
||||
flexmock(module.load).should_receive('load_configuration').and_return(source_config)
|
||||
flexmock(module.normalize).should_receive('normalize')
|
||||
flexmock(module).should_receive('schema_to_sample_configuration').with_args(
|
||||
object, source_config
|
||||
).once()
|
||||
flexmock(module).should_receive('merge_source_configuration_into_destination')
|
||||
flexmock(module).should_receive('render_configuration')
|
||||
flexmock(module).should_receive('comment_out_optional_configuration')
|
||||
|
||||
@@ -601,6 +601,53 @@ def test_collect_spot_check_source_paths_parses_borg_output():
|
||||
) == ('/etc/path', '/etc/other')
|
||||
|
||||
|
||||
def test_collect_spot_check_source_paths_omits_progress_from_create_dry_run_command():
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return(
|
||||
{'hook1': False, 'hook2': False}
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
|
||||
flexmock()
|
||||
)
|
||||
flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(
|
||||
flexmock()
|
||||
)
|
||||
flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return(
|
||||
[Pattern('foo'), Pattern('bar')]
|
||||
)
|
||||
flexmock(module.borgmatic.borg.create).should_receive('make_base_create_command').with_args(
|
||||
dry_run=True,
|
||||
repository_path='repo',
|
||||
config={'working_directory': '/', 'list_details': True},
|
||||
patterns=[Pattern('foo'), Pattern('bar')],
|
||||
local_borg_version=object,
|
||||
global_arguments=object,
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
local_path=object,
|
||||
remote_path=object,
|
||||
stream_processes=False,
|
||||
).and_return((('borg', 'create'), ('repo::archive',), flexmock()))
|
||||
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
|
||||
flexmock()
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.execute).should_receive(
|
||||
'execute_command_and_capture_output'
|
||||
).and_return(
|
||||
'warning: stuff\n- /etc/path\n+ /etc/other\n? /nope',
|
||||
)
|
||||
flexmock(module.os.path).should_receive('isfile').and_return(True)
|
||||
|
||||
assert module.collect_spot_check_source_paths(
|
||||
repository={'path': 'repo'},
|
||||
config={'working_directory': '/', 'progress': True},
|
||||
local_borg_version=flexmock(),
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
) == ('/etc/path', '/etc/other')
|
||||
|
||||
|
||||
def test_collect_spot_check_source_paths_passes_through_stream_processes_false():
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return(
|
||||
{'hook1': False, 'hook2': False}
|
||||
@@ -891,8 +938,8 @@ def test_collect_spot_check_source_paths_uses_working_directory():
|
||||
|
||||
|
||||
def test_compare_spot_check_hashes_returns_paths_having_failing_hashes():
|
||||
flexmock(module.random).should_receive('sample').replace_with(
|
||||
lambda population, count: population[:count]
|
||||
flexmock(module.random).should_receive('SystemRandom').and_return(
|
||||
flexmock(sample=lambda population, count: population[:count])
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
|
||||
None,
|
||||
@@ -932,8 +979,8 @@ def test_compare_spot_check_hashes_returns_paths_having_failing_hashes():
|
||||
|
||||
|
||||
def test_compare_spot_check_hashes_returns_relative_paths_having_failing_hashes():
|
||||
flexmock(module.random).should_receive('sample').replace_with(
|
||||
lambda population, count: population[:count]
|
||||
flexmock(module.random).should_receive('SystemRandom').and_return(
|
||||
flexmock(sample=lambda population, count: population[:count])
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
|
||||
None,
|
||||
@@ -973,8 +1020,8 @@ def test_compare_spot_check_hashes_returns_relative_paths_having_failing_hashes(
|
||||
|
||||
|
||||
def test_compare_spot_check_hashes_handles_data_sample_percentage_above_100():
|
||||
flexmock(module.random).should_receive('sample').replace_with(
|
||||
lambda population, count: population[:count]
|
||||
flexmock(module.random).should_receive('SystemRandom').and_return(
|
||||
flexmock(sample=lambda population, count: population[:count])
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
|
||||
None,
|
||||
@@ -1014,8 +1061,8 @@ def test_compare_spot_check_hashes_handles_data_sample_percentage_above_100():
|
||||
|
||||
|
||||
def test_compare_spot_check_hashes_uses_xxh64sum_command_option():
|
||||
flexmock(module.random).should_receive('sample').replace_with(
|
||||
lambda population, count: population[:count]
|
||||
flexmock(module.random).should_receive('SystemRandom').and_return(
|
||||
flexmock(sample=lambda population, count: population[:count])
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
|
||||
None,
|
||||
@@ -1024,7 +1071,9 @@ def test_compare_spot_check_hashes_uses_xxh64sum_command_option():
|
||||
flexmock(module.os.path).should_receive('islink').and_return(False)
|
||||
flexmock(module.borgmatic.execute).should_receive(
|
||||
'execute_command_and_capture_output'
|
||||
).with_args(('/usr/local/bin/xxh64sum', '/foo', '/bar'), working_directory=None).and_return(
|
||||
).with_args(
|
||||
('/usr/local/bin/xxhsum', '-H64', '/foo', '/bar'), working_directory=None
|
||||
).and_return(
|
||||
'hash1 /foo\nhash2 /bar'
|
||||
)
|
||||
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
|
||||
@@ -1039,7 +1088,7 @@ def test_compare_spot_check_hashes_uses_xxh64sum_command_option():
|
||||
{
|
||||
'name': 'spot',
|
||||
'data_sample_percentage': 50,
|
||||
'xxh64sum_command': '/usr/local/bin/xxh64sum',
|
||||
'xxh64sum_command': '/usr/local/bin/xxhsum -H64',
|
||||
},
|
||||
]
|
||||
},
|
||||
@@ -1052,8 +1101,8 @@ def test_compare_spot_check_hashes_uses_xxh64sum_command_option():
|
||||
|
||||
|
||||
def test_compare_spot_check_hashes_considers_path_missing_from_archive_as_not_matching():
|
||||
flexmock(module.random).should_receive('sample').replace_with(
|
||||
lambda population, count: population[:count]
|
||||
flexmock(module.random).should_receive('SystemRandom').and_return(
|
||||
flexmock(sample=lambda population, count: population[:count])
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
|
||||
None,
|
||||
@@ -1089,8 +1138,8 @@ def test_compare_spot_check_hashes_considers_path_missing_from_archive_as_not_ma
|
||||
|
||||
|
||||
def test_compare_spot_check_hashes_considers_symlink_path_as_not_matching():
|
||||
flexmock(module.random).should_receive('sample').replace_with(
|
||||
lambda population, count: population[:count]
|
||||
flexmock(module.random).should_receive('SystemRandom').and_return(
|
||||
flexmock(sample=lambda population, count: population[:count])
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
|
||||
None,
|
||||
@@ -1125,8 +1174,8 @@ def test_compare_spot_check_hashes_considers_symlink_path_as_not_matching():
|
||||
|
||||
|
||||
def test_compare_spot_check_hashes_considers_non_existent_path_as_not_matching():
|
||||
flexmock(module.random).should_receive('sample').replace_with(
|
||||
lambda population, count: population[:count]
|
||||
flexmock(module.random).should_receive('SystemRandom').and_return(
|
||||
flexmock(sample=lambda population, count: population[:count])
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
|
||||
None,
|
||||
@@ -1162,8 +1211,8 @@ def test_compare_spot_check_hashes_considers_non_existent_path_as_not_matching()
|
||||
|
||||
def test_compare_spot_check_hashes_with_too_many_paths_feeds_them_to_commands_in_chunks():
|
||||
flexmock(module).SAMPLE_PATHS_SUBSET_COUNT = 2
|
||||
flexmock(module.random).should_receive('sample').replace_with(
|
||||
lambda population, count: population[:count]
|
||||
flexmock(module.random).should_receive('SystemRandom').and_return(
|
||||
flexmock(sample=lambda population, count: population[:count])
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
|
||||
None,
|
||||
@@ -1208,8 +1257,8 @@ def test_compare_spot_check_hashes_with_too_many_paths_feeds_them_to_commands_in
|
||||
|
||||
|
||||
def test_compare_spot_check_hashes_uses_working_directory_to_access_source_paths():
|
||||
flexmock(module.random).should_receive('sample').replace_with(
|
||||
lambda population, count: population[:count]
|
||||
flexmock(module.random).should_receive('SystemRandom').and_return(
|
||||
flexmock(sample=lambda population, count: population[:count])
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
|
||||
'/working/dir',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
@@ -25,6 +26,7 @@ def test_run_create_executes_and_calls_hooks_for_configured_repository():
|
||||
progress=flexmock(),
|
||||
statistics=flexmock(),
|
||||
json=False,
|
||||
comment=None,
|
||||
list_details=flexmock(),
|
||||
)
|
||||
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
|
||||
@@ -66,6 +68,7 @@ def test_run_create_runs_with_selected_repository():
|
||||
progress=flexmock(),
|
||||
statistics=flexmock(),
|
||||
json=False,
|
||||
comment=None,
|
||||
list_details=flexmock(),
|
||||
)
|
||||
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
|
||||
@@ -98,6 +101,7 @@ def test_run_create_bails_if_repository_does_not_match():
|
||||
progress=flexmock(),
|
||||
statistics=flexmock(),
|
||||
json=False,
|
||||
comment=None,
|
||||
list_details=flexmock(),
|
||||
)
|
||||
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
|
||||
@@ -130,6 +134,7 @@ def test_run_create_with_both_list_and_json_errors():
|
||||
progress=flexmock(),
|
||||
statistics=flexmock(),
|
||||
json=True,
|
||||
comment=None,
|
||||
list_details=flexmock(),
|
||||
)
|
||||
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
|
||||
@@ -163,6 +168,7 @@ def test_run_create_with_both_list_and_progress_errors():
|
||||
progress=flexmock(),
|
||||
statistics=flexmock(),
|
||||
json=False,
|
||||
comment=None,
|
||||
list_details=flexmock(),
|
||||
)
|
||||
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
|
||||
@@ -209,6 +215,7 @@ def test_run_create_produces_json():
|
||||
progress=flexmock(),
|
||||
statistics=flexmock(),
|
||||
json=True,
|
||||
comment=None,
|
||||
list_details=flexmock(),
|
||||
)
|
||||
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
|
||||
@@ -227,3 +234,193 @@ def test_run_create_produces_json():
|
||||
remote_path=None,
|
||||
)
|
||||
) == [parsed_json]
|
||||
|
||||
|
||||
def test_run_create_with_active_dumps_roundtrips_via_checkpoint_archive():
|
||||
mock_dump_process = flexmock()
|
||||
mock_dump_process.should_receive('poll').and_return(None).and_return(0)
|
||||
|
||||
flexmock(module.logger).answer = lambda message: None
|
||||
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
flexmock()
|
||||
)
|
||||
flexmock(module.borgmatic.borg.create).should_receive('create_archive').once()
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return(
|
||||
{'dump': mock_dump_process}
|
||||
)
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive(
|
||||
'call_hooks_even_if_unconfigured'
|
||||
).and_return({})
|
||||
flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(())
|
||||
flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return([])
|
||||
flexmock(os.path).should_receive('join').and_return('/run/borgmatic/bootstrap')
|
||||
flexmock(module.borgmatic.borg.repo_list).should_receive('get_latest_archive').and_return(
|
||||
{'id': 'id1', 'name': 'archive.checkpoint'}
|
||||
)
|
||||
|
||||
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
|
||||
|
||||
flexmock(module).should_receive('rename_checkpoint_archive').with_args(
|
||||
repository_path='repo',
|
||||
global_arguments=global_arguments,
|
||||
config={},
|
||||
local_borg_version=None,
|
||||
local_path=None,
|
||||
remote_path=None,
|
||||
).once()
|
||||
create_arguments = flexmock(
|
||||
repository=None,
|
||||
progress=flexmock(),
|
||||
statistics=flexmock(),
|
||||
json=False,
|
||||
comment=None,
|
||||
list_details=flexmock(),
|
||||
)
|
||||
|
||||
list(
|
||||
module.run_create(
|
||||
config_filename='test.yaml',
|
||||
repository={'path': 'repo'},
|
||||
config={},
|
||||
config_paths=['/tmp/test.yaml'],
|
||||
local_borg_version=None,
|
||||
create_arguments=create_arguments,
|
||||
global_arguments=global_arguments,
|
||||
dry_run_label='',
|
||||
local_path=None,
|
||||
remote_path=None,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_run_create_with_active_dumps_json_updates_archive_info():
|
||||
mock_dump_process = flexmock()
|
||||
mock_dump_process.should_receive('poll').and_return(None).and_return(0)
|
||||
|
||||
borg_create_result = {
|
||||
'archive': {
|
||||
'command_line': ['foo'],
|
||||
'name': 'archive.checkpoint',
|
||||
'id': 'id1',
|
||||
},
|
||||
'cache': {},
|
||||
'repository': {
|
||||
'id': 'repo-id',
|
||||
},
|
||||
}
|
||||
|
||||
expected_create_result = {
|
||||
'archive': {
|
||||
'command_line': ['foo'],
|
||||
'name': 'archive',
|
||||
'id': 'id2',
|
||||
},
|
||||
'cache': {},
|
||||
'repository': {'id': 'repo-id', 'label': ''},
|
||||
}
|
||||
|
||||
flexmock(module.logger).answer = lambda message: None
|
||||
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
flexmock()
|
||||
)
|
||||
|
||||
flexmock(module.borgmatic.borg.create).should_receive('create_archive').and_return(
|
||||
json.dumps(borg_create_result)
|
||||
).once()
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return(
|
||||
{'dump': mock_dump_process}
|
||||
)
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive(
|
||||
'call_hooks_even_if_unconfigured'
|
||||
).and_return({})
|
||||
flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(())
|
||||
flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return([])
|
||||
flexmock(os.path).should_receive('join').and_return('/run/borgmatic/bootstrap')
|
||||
|
||||
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
|
||||
|
||||
flexmock(module).should_receive('rename_checkpoint_archive').with_args(
|
||||
repository_path='repo',
|
||||
global_arguments=global_arguments,
|
||||
config={},
|
||||
local_borg_version=None,
|
||||
local_path=None,
|
||||
remote_path=None,
|
||||
).once()
|
||||
|
||||
flexmock(module.borgmatic.borg.repo_list).should_receive('get_latest_archive').and_return(
|
||||
{'id': 'id2', 'name': 'archive'},
|
||||
)
|
||||
|
||||
create_arguments = flexmock(
|
||||
repository=None,
|
||||
progress=flexmock(),
|
||||
statistics=flexmock(),
|
||||
json=True,
|
||||
comment=None,
|
||||
list_details=flexmock(),
|
||||
)
|
||||
|
||||
assert list(
|
||||
module.run_create(
|
||||
config_filename='test.yaml',
|
||||
repository={'path': 'repo'},
|
||||
config={},
|
||||
config_paths=['/tmp/test.yaml'],
|
||||
local_borg_version=None,
|
||||
create_arguments=create_arguments,
|
||||
global_arguments=global_arguments,
|
||||
dry_run_label='',
|
||||
local_path=None,
|
||||
remote_path=None,
|
||||
)
|
||||
) == [expected_create_result]
|
||||
|
||||
|
||||
def test_rename_checkpoint_archive_renames_archive():
|
||||
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
|
||||
flexmock(module.borgmatic.borg.repo_list).should_receive('get_latest_archive').and_return(
|
||||
{'id': 'id1', 'name': 'archive.checkpoint'}
|
||||
)
|
||||
|
||||
flexmock(module.borgmatic.borg.rename).should_receive('rename_archive').with_args(
|
||||
repository_name='path',
|
||||
old_archive_name='archive.checkpoint',
|
||||
new_archive_name='archive',
|
||||
dry_run=False,
|
||||
config={},
|
||||
local_borg_version=None,
|
||||
local_path=None,
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
module.rename_checkpoint_archive(
|
||||
repository_path='path',
|
||||
global_arguments=global_arguments,
|
||||
config={},
|
||||
local_borg_version=None,
|
||||
local_path=None,
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
|
||||
def test_rename_checkpoint_archive_checks_suffix():
|
||||
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
|
||||
flexmock(module.borgmatic.borg.repo_list).should_receive('get_latest_archive').and_return(
|
||||
{'id': 'id1', 'name': 'unexpected-archive'}
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match='Latest archive did not have a .checkpoint suffix. Got: unexpected-archive',
|
||||
):
|
||||
module.rename_checkpoint_archive(
|
||||
repository_path='path',
|
||||
global_arguments=global_arguments,
|
||||
config={},
|
||||
local_borg_version=None,
|
||||
local_path=None,
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
@@ -911,12 +911,14 @@ def test_check_archives_with_retention_prefix():
|
||||
|
||||
|
||||
def test_check_archives_with_extra_borg_options_passes_through_to_borg():
|
||||
config = {'extra_borg_options': {'check': '--extra --options'}}
|
||||
config = {'extra_borg_options': {'check': '--extra --options "value with space"'}}
|
||||
flexmock(module).should_receive('make_check_name_flags').with_args(
|
||||
{'repository'}, ()
|
||||
).and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(('borg', 'check', '--extra', '--options', 'repo'))
|
||||
insert_execute_command_mock(
|
||||
('borg', 'check', '--extra', '--options', 'value with space', 'repo')
|
||||
)
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
|
||||
@@ -232,12 +232,14 @@ def test_compact_segments_with_lock_wait_calls_borg_with_lock_wait_flags():
|
||||
|
||||
def test_compact_segments_with_extra_borg_options_calls_borg_with_extra_options():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(COMPACT_COMMAND + ('--extra', '--options', 'repo'), logging.INFO)
|
||||
insert_execute_command_mock(
|
||||
COMPACT_COMMAND + ('--extra', '--options', 'value with space', 'repo'), logging.INFO
|
||||
)
|
||||
|
||||
module.compact_segments(
|
||||
dry_run=False,
|
||||
repository_path='repo',
|
||||
config={'extra_borg_options': {'compact': '--extra --options'}},
|
||||
config={'extra_borg_options': {'compact': '--extra --options "value with space"'}},
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
)
|
||||
|
||||
@@ -392,6 +392,39 @@ def test_make_base_create_command_includes_dry_run_in_borg_command():
|
||||
assert not pattern_file
|
||||
|
||||
|
||||
def test_make_base_create_command_includes_comment_in_borg_command():
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO')
|
||||
flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
|
||||
'{hostname}'
|
||||
)
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
||||
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
|
||||
)
|
||||
|
||||
(create_flags, create_positional_arguments, pattern_file) = module.make_base_create_command(
|
||||
dry_run=False,
|
||||
repository_path='repo',
|
||||
config={
|
||||
'source_directories': ['foo', 'bar'],
|
||||
'repositories': ['repo'],
|
||||
'exclude_patterns': ['exclude'],
|
||||
},
|
||||
patterns=[Pattern('foo'), Pattern('bar')],
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
comment='a comment',
|
||||
)
|
||||
|
||||
assert create_flags == ('borg', 'create', '--comment', 'a comment')
|
||||
assert create_positional_arguments == REPO_ARCHIVE
|
||||
assert not pattern_file
|
||||
|
||||
|
||||
def test_make_base_create_command_includes_local_path_in_borg_command():
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
|
||||
@@ -786,6 +819,34 @@ def test_make_base_create_command_includes_repository_and_archive_name_format_wi
|
||||
assert not pattern_file
|
||||
|
||||
|
||||
def test_make_base_create_command_includes_archive_suffix_in_borg_command():
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO')
|
||||
flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
|
||||
DEFAULT_ARCHIVE_NAME
|
||||
)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
|
||||
(create_flags, create_positional_arguments, pattern_file) = module.make_base_create_command(
|
||||
dry_run=False,
|
||||
repository_path='repo',
|
||||
config={
|
||||
'source_directories': ['foo', 'bar'],
|
||||
'repositories': ['repo'],
|
||||
},
|
||||
patterns=[Pattern('foo'), Pattern('bar')],
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
archive_suffix='.checkpoint',
|
||||
)
|
||||
|
||||
assert create_flags == ('borg', 'create')
|
||||
assert create_positional_arguments == (f'repo::{DEFAULT_ARCHIVE_NAME}.checkpoint',)
|
||||
assert not pattern_file
|
||||
|
||||
|
||||
def test_make_base_create_command_includes_extra_borg_options_in_borg_command():
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
|
||||
@@ -805,7 +866,7 @@ def test_make_base_create_command_includes_extra_borg_options_in_borg_command():
|
||||
config={
|
||||
'source_directories': ['foo', 'bar'],
|
||||
'repositories': ['repo'],
|
||||
'extra_borg_options': {'create': '--extra --options'},
|
||||
'extra_borg_options': {'create': '--extra --options "value with space"'},
|
||||
},
|
||||
patterns=[Pattern('foo'), Pattern('bar')],
|
||||
local_borg_version='1.2.3',
|
||||
@@ -813,7 +874,7 @@ def test_make_base_create_command_includes_extra_borg_options_in_borg_command():
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
assert create_flags == ('borg', 'create', '--extra', '--options')
|
||||
assert create_flags == ('borg', 'create', '--extra', '--options', 'value with space')
|
||||
assert create_positional_arguments == REPO_ARCHIVE
|
||||
assert not pattern_file
|
||||
|
||||
@@ -1414,6 +1475,41 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_flag():
|
||||
assert json_output == '[]'
|
||||
|
||||
|
||||
def test_create_archive_with_comment_calls_borg_with_comment_flag():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module).should_receive('make_base_create_command').and_return(
|
||||
(('borg', 'create', '--comment', 'a comment'), REPO_ARCHIVE, flexmock())
|
||||
)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--comment', 'a comment') + REPO_ARCHIVE,
|
||||
output_log_level=logging.INFO,
|
||||
output_file=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
working_directory=None,
|
||||
environment=None,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
dry_run=False,
|
||||
repository_path='repo',
|
||||
config={
|
||||
'source_directories': ['foo', 'bar'],
|
||||
'repositories': ['repo'],
|
||||
'exclude_patterns': None,
|
||||
},
|
||||
patterns=[Pattern('foo'), Pattern('bar')],
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
borgmatic_runtime_directory='/borgmatic/run',
|
||||
json=False,
|
||||
comment='a comment',
|
||||
)
|
||||
|
||||
|
||||
def test_create_archive_calls_borg_with_working_directory():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
|
||||
@@ -90,9 +90,11 @@ def test_make_environment_without_configuration_sets_certain_environment_variabl
|
||||
assert environment == {
|
||||
'USER': 'root',
|
||||
'BORG_EXIT_CODES': 'modern',
|
||||
'BORG_RELOCATED_REPO_ACCESS_IS_OK': 'no',
|
||||
'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK': 'no',
|
||||
'BORG_USE_CHUNKS_ARCHIVE': 'no',
|
||||
'BORG_RELOCATED_REPO_ACCESS_IS_OK': 'NO',
|
||||
'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK': 'NO',
|
||||
'BORG_USE_CHUNKS_ARCHIVE': 'NO',
|
||||
'BORG_DEBUG_PASSPHRASE': 'NO',
|
||||
'BORG_DISPLAY_PASSPHRASE': 'NO',
|
||||
}
|
||||
|
||||
|
||||
@@ -103,6 +105,8 @@ def test_make_environment_without_configuration_passes_through_default_environme
|
||||
'BORG_RELOCATED_REPO_ACCESS_IS_OK': 'yup',
|
||||
'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK': 'nah',
|
||||
'BORG_USE_CHUNKS_ARCHIVE': 'yup',
|
||||
'BORG_DEBUG_PASSPHRASE': 'nah',
|
||||
'BORG_DISPLAY_PASSPHRASE': 'yup',
|
||||
}
|
||||
)
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
@@ -116,11 +120,13 @@ def test_make_environment_without_configuration_passes_through_default_environme
|
||||
'BORG_RELOCATED_REPO_ACCESS_IS_OK': 'yup',
|
||||
'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK': 'nah',
|
||||
'BORG_USE_CHUNKS_ARCHIVE': 'yup',
|
||||
'BORG_DEBUG_PASSPHRASE': 'nah',
|
||||
'BORG_DISPLAY_PASSPHRASE': 'yup',
|
||||
'BORG_EXIT_CODES': 'modern',
|
||||
}
|
||||
|
||||
|
||||
def test_make_environment_with_relocated_repo_access_true_should_set_environment_yes():
|
||||
def test_make_environment_with_relocated_repo_access_true_should_set_environment_YES():
|
||||
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
@@ -128,10 +134,10 @@ def test_make_environment_with_relocated_repo_access_true_should_set_environment
|
||||
flexmock(module.os).should_receive('pipe').never()
|
||||
environment = module.make_environment({'relocated_repo_access_is_ok': True})
|
||||
|
||||
assert environment.get('BORG_RELOCATED_REPO_ACCESS_IS_OK') == 'yes'
|
||||
assert environment.get('BORG_RELOCATED_REPO_ACCESS_IS_OK') == 'YES'
|
||||
|
||||
|
||||
def test_make_environment_with_relocated_repo_access_false_should_set_environment_no():
|
||||
def test_make_environment_with_relocated_repo_access_false_should_set_environment_NO():
|
||||
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
@@ -139,7 +145,7 @@ def test_make_environment_with_relocated_repo_access_false_should_set_environmen
|
||||
flexmock(module.os).should_receive('pipe').never()
|
||||
environment = module.make_environment({'relocated_repo_access_is_ok': False})
|
||||
|
||||
assert environment.get('BORG_RELOCATED_REPO_ACCESS_IS_OK') == 'no'
|
||||
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():
|
||||
@@ -164,6 +170,50 @@ def test_make_environment_check_i_know_what_i_am_doing_false_should_set_environm
|
||||
assert environment.get('BORG_CHECK_I_KNOW_WHAT_I_AM_DOING') == 'NO'
|
||||
|
||||
|
||||
def test_make_environment_debug_passphrase_true_should_set_environment_YES():
|
||||
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()
|
||||
environment = module.make_environment({'debug_passphrase': True})
|
||||
|
||||
assert environment.get('BORG_DEBUG_PASSPHRASE') == 'YES'
|
||||
|
||||
|
||||
def test_make_environment_debug_passphrase_false_should_set_environment_NO():
|
||||
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()
|
||||
environment = module.make_environment({'debug_passphrase': False})
|
||||
|
||||
assert environment.get('BORG_DEBUG_PASSPHRASE') == 'NO'
|
||||
|
||||
|
||||
def test_make_environment_display_passphrase_true_should_set_environment_YES():
|
||||
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()
|
||||
environment = module.make_environment({'display_passphrase': True})
|
||||
|
||||
assert environment.get('BORG_DISPLAY_PASSPHRASE') == 'YES'
|
||||
|
||||
|
||||
def test_make_environment_display_passphrase_false_should_set_environment_NO():
|
||||
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()
|
||||
environment = module.make_environment({'display_passphrase': False})
|
||||
|
||||
assert environment.get('BORG_DISPLAY_PASSPHRASE') == 'NO'
|
||||
|
||||
|
||||
def test_make_environment_with_integer_variable_value():
|
||||
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
@@ -183,7 +233,7 @@ def test_make_environment_with_use_chunks_archive_should_set_correct_environment
|
||||
flexmock(module.os).should_receive('pipe').never()
|
||||
|
||||
environment = module.make_environment({'use_chunks_archive': True})
|
||||
assert environment.get('BORG_USE_CHUNKS_ARCHIVE') == 'yes'
|
||||
assert environment.get('BORG_USE_CHUNKS_ARCHIVE') == 'YES'
|
||||
|
||||
environment = module.make_environment({'use_chunks_archive': False})
|
||||
assert environment.get('BORG_USE_CHUNKS_ARCHIVE') == 'no'
|
||||
assert environment.get('BORG_USE_CHUNKS_ARCHIVE') == 'NO'
|
||||
|
||||
@@ -472,13 +472,15 @@ def test_prune_archives_with_extra_borg_options_calls_borg_with_extra_options():
|
||||
flexmock(module.feature).should_receive('available').with_args(
|
||||
module.feature.Feature.NO_PRUNE_STATS, '1.2.3'
|
||||
).and_return(False)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--extra', '--options', 'repo'), logging.INFO)
|
||||
insert_execute_command_mock(
|
||||
PRUNE_COMMAND + ('--extra', '--options', 'value with space', 'repo'), logging.INFO
|
||||
)
|
||||
|
||||
prune_arguments = flexmock(statistics=False, list_details=False)
|
||||
module.prune_archives(
|
||||
dry_run=False,
|
||||
repository_path='repo',
|
||||
config={'extra_borg_options': {'prune': '--extra --options'}},
|
||||
config={'extra_borg_options': {'prune': '--extra --options "value with space"'}},
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
prune_arguments=prune_arguments,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import logging
|
||||
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.borg import rename as module
|
||||
|
||||
|
||||
def test_rename_archive_calls_borg_rename():
|
||||
environment = flexmock()
|
||||
|
||||
# Note: make_rename_command is tested as integration test.
|
||||
flexmock(module).should_receive('make_rename_command').and_return(('borg', 'fake-command'))
|
||||
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
|
||||
environment
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
|
||||
'/working/dir'
|
||||
)
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
('borg', 'fake-command'),
|
||||
output_log_level=logging.INFO,
|
||||
environment=environment,
|
||||
working_directory='/working/dir',
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
|
||||
module.rename_archive(
|
||||
dry_run=False,
|
||||
repository_name='repo',
|
||||
old_archive_name='old_archive',
|
||||
new_archive_name='new_archive',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
@@ -484,7 +484,7 @@ def test_create_repository_with_umask_calls_borg_with_umask_flag():
|
||||
def test_create_repository_with_extra_borg_options_calls_borg_with_extra_options():
|
||||
insert_repo_info_command_not_found_mock()
|
||||
insert_repo_create_command_mock(
|
||||
REPO_CREATE_COMMAND + ('--extra', '--options', '--repo', 'repo')
|
||||
REPO_CREATE_COMMAND + ('--extra', '--options', 'value with space', '--repo', 'repo')
|
||||
)
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(
|
||||
@@ -497,7 +497,7 @@ def test_create_repository_with_extra_borg_options_calls_borg_with_extra_options
|
||||
module.create_repository(
|
||||
dry_run=False,
|
||||
repository_path='repo',
|
||||
config={'extra_borg_options': {'repo-create': '--extra --options'}},
|
||||
config={'extra_borg_options': {'repo-create': '--extra --options "value with space"'}},
|
||||
local_borg_version='2.3.4',
|
||||
global_arguments=flexmock(),
|
||||
encryption_mode='repokey',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
@@ -11,7 +12,7 @@ from ..test_verbosity import insert_logging_mock
|
||||
BORG_LIST_LATEST_ARGUMENTS = (
|
||||
'--last',
|
||||
'1',
|
||||
'--short',
|
||||
'--json',
|
||||
'repo',
|
||||
)
|
||||
|
||||
@@ -31,8 +32,43 @@ def test_resolve_archive_name_passes_through_non_latest_archive_name():
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_archive_name_calls_borg_with_flags():
|
||||
def test_resolve_archive_name_calls_get_latest_archive():
|
||||
expected_archive = 'archive-name'
|
||||
|
||||
repository_path = flexmock()
|
||||
config = flexmock()
|
||||
local_borg_version = flexmock()
|
||||
global_arguments = flexmock()
|
||||
local_path = flexmock()
|
||||
remote_path = flexmock()
|
||||
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('get_latest_archive').with_args(
|
||||
repository_path,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
).and_return({'name': expected_archive})
|
||||
|
||||
assert (
|
||||
module.resolve_archive_name(
|
||||
repository_path,
|
||||
'latest',
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
== expected_archive
|
||||
)
|
||||
|
||||
|
||||
def test_get_latest_archive_calls_borg_with_flags():
|
||||
expected_archive = {'name': 'archive-name'}
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
@@ -41,12 +77,11 @@ def test_resolve_archive_name_calls_borg_with_flags():
|
||||
borg_exit_codes=None,
|
||||
environment=None,
|
||||
working_directory=None,
|
||||
).and_return(expected_archive + '\n')
|
||||
).and_return(json.dumps({'archives': [expected_archive]}))
|
||||
|
||||
assert (
|
||||
module.resolve_archive_name(
|
||||
module.get_latest_archive(
|
||||
'repo',
|
||||
'latest',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
@@ -55,8 +90,8 @@ def test_resolve_archive_name_calls_borg_with_flags():
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_archive_name_with_log_info_calls_borg_without_info_flag():
|
||||
expected_archive = 'archive-name'
|
||||
def test_get_latest_archive_with_log_info_calls_borg_without_info_flag():
|
||||
expected_archive = {'name': 'archive-name'}
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
@@ -65,13 +100,12 @@ def test_resolve_archive_name_with_log_info_calls_borg_without_info_flag():
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).and_return(expected_archive + '\n')
|
||||
).and_return(json.dumps({'archives': [expected_archive]}))
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
assert (
|
||||
module.resolve_archive_name(
|
||||
module.get_latest_archive(
|
||||
'repo',
|
||||
'latest',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
@@ -80,8 +114,8 @@ def test_resolve_archive_name_with_log_info_calls_borg_without_info_flag():
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_archive_name_with_log_debug_calls_borg_without_debug_flag():
|
||||
expected_archive = 'archive-name'
|
||||
def test_get_latest_archive_with_log_debug_calls_borg_without_debug_flag():
|
||||
expected_archive = {'name': 'archive-name'}
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
@@ -90,13 +124,12 @@ def test_resolve_archive_name_with_log_debug_calls_borg_without_debug_flag():
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).and_return(expected_archive + '\n')
|
||||
).and_return(json.dumps({'archives': [expected_archive]}))
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
assert (
|
||||
module.resolve_archive_name(
|
||||
module.get_latest_archive(
|
||||
'repo',
|
||||
'latest',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
@@ -105,8 +138,8 @@ def test_resolve_archive_name_with_log_debug_calls_borg_without_debug_flag():
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_archive_name_with_local_path_calls_borg_via_local_path():
|
||||
expected_archive = 'archive-name'
|
||||
def test_get_latest_archive_with_local_path_calls_borg_via_local_path():
|
||||
expected_archive = {'name': 'archive-name'}
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
@@ -115,12 +148,11 @@ def test_resolve_archive_name_with_local_path_calls_borg_via_local_path():
|
||||
working_directory=None,
|
||||
borg_local_path='borg1',
|
||||
borg_exit_codes=None,
|
||||
).and_return(expected_archive + '\n')
|
||||
).and_return(json.dumps({'archives': [expected_archive]}))
|
||||
|
||||
assert (
|
||||
module.resolve_archive_name(
|
||||
module.get_latest_archive(
|
||||
'repo',
|
||||
'latest',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
@@ -130,8 +162,8 @@ def test_resolve_archive_name_with_local_path_calls_borg_via_local_path():
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_archive_name_with_exit_codes_calls_borg_using_them():
|
||||
expected_archive = 'archive-name'
|
||||
def test_get_latest_archive_with_exit_codes_calls_borg_using_them():
|
||||
expected_archive = {'name': 'archive-name'}
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
borg_exit_codes = flexmock()
|
||||
@@ -141,12 +173,11 @@ def test_resolve_archive_name_with_exit_codes_calls_borg_using_them():
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=borg_exit_codes,
|
||||
).and_return(expected_archive + '\n')
|
||||
).and_return(json.dumps({'archives': [expected_archive]}))
|
||||
|
||||
assert (
|
||||
module.resolve_archive_name(
|
||||
module.get_latest_archive(
|
||||
'repo',
|
||||
'latest',
|
||||
config={'borg_exit_codes': borg_exit_codes},
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
@@ -155,8 +186,8 @@ def test_resolve_archive_name_with_exit_codes_calls_borg_using_them():
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_archive_name_with_remote_path_calls_borg_with_remote_path_flags():
|
||||
expected_archive = 'archive-name'
|
||||
def test_get_latest_archive_with_remote_path_calls_borg_with_remote_path_flags():
|
||||
expected_archive = {'name': 'archive-name'}
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
@@ -165,12 +196,11 @@ def test_resolve_archive_name_with_remote_path_calls_borg_with_remote_path_flags
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).and_return(expected_archive + '\n')
|
||||
).and_return(json.dumps({'archives': [expected_archive]}))
|
||||
|
||||
assert (
|
||||
module.resolve_archive_name(
|
||||
module.get_latest_archive(
|
||||
'repo',
|
||||
'latest',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
@@ -180,8 +210,8 @@ def test_resolve_archive_name_with_remote_path_calls_borg_with_remote_path_flags
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_archive_name_with_umask_calls_borg_with_umask_flags():
|
||||
expected_archive = 'archive-name'
|
||||
def test_get_latest_archive_with_umask_calls_borg_with_umask_flags():
|
||||
expected_archive = {'name': 'archive-name'}
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
@@ -190,12 +220,11 @@ def test_resolve_archive_name_with_umask_calls_borg_with_umask_flags():
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).and_return(expected_archive + '\n')
|
||||
).and_return(json.dumps({'archives': [expected_archive]}))
|
||||
|
||||
assert (
|
||||
module.resolve_archive_name(
|
||||
module.get_latest_archive(
|
||||
'repo',
|
||||
'latest',
|
||||
config={'umask': '077'},
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
@@ -204,7 +233,7 @@ def test_resolve_archive_name_with_umask_calls_borg_with_umask_flags():
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_archive_name_without_archives_raises():
|
||||
def test_get_latest_archive_without_archives_raises():
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
@@ -213,20 +242,19 @@ def test_resolve_archive_name_without_archives_raises():
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).and_return('')
|
||||
).and_return(json.dumps({'archives': []}))
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.resolve_archive_name(
|
||||
module.get_latest_archive(
|
||||
'repo',
|
||||
'latest',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_archive_name_with_log_json_calls_borg_with_log_json_flags():
|
||||
expected_archive = 'archive-name'
|
||||
def test_get_latest_archive_with_log_json_calls_borg_with_log_json_flags():
|
||||
expected_archive = {'name': 'archive-name'}
|
||||
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -236,12 +264,11 @@ def test_resolve_archive_name_with_log_json_calls_borg_with_log_json_flags():
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).and_return(expected_archive + '\n')
|
||||
).and_return(json.dumps({'archives': [expected_archive]}))
|
||||
|
||||
assert (
|
||||
module.resolve_archive_name(
|
||||
module.get_latest_archive(
|
||||
'repo',
|
||||
'latest',
|
||||
config={'log_json': True},
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
@@ -250,8 +277,8 @@ def test_resolve_archive_name_with_log_json_calls_borg_with_log_json_flags():
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_archive_name_with_lock_wait_calls_borg_with_lock_wait_flags():
|
||||
expected_archive = 'archive-name'
|
||||
def test_get_latest_archive_with_lock_wait_calls_borg_with_lock_wait_flags():
|
||||
expected_archive = {'name': 'archive-name'}
|
||||
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -261,12 +288,11 @@ def test_resolve_archive_name_with_lock_wait_calls_borg_with_lock_wait_flags():
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).and_return(expected_archive + '\n')
|
||||
).and_return(json.dumps({'archives': [expected_archive]}))
|
||||
|
||||
assert (
|
||||
module.resolve_archive_name(
|
||||
module.get_latest_archive(
|
||||
'repo',
|
||||
'latest',
|
||||
config={'lock_wait': 'okay'},
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
@@ -275,8 +301,8 @@ def test_resolve_archive_name_with_lock_wait_calls_borg_with_lock_wait_flags():
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_archive_name_calls_borg_with_working_directory():
|
||||
expected_archive = 'archive-name'
|
||||
def test_get_latest_archive_calls_borg_with_working_directory():
|
||||
expected_archive = {'name': 'archive-name'}
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
|
||||
'/working/dir',
|
||||
@@ -287,12 +313,11 @@ def test_resolve_archive_name_calls_borg_with_working_directory():
|
||||
borg_exit_codes=None,
|
||||
environment=None,
|
||||
working_directory='/working/dir',
|
||||
).and_return(expected_archive + '\n')
|
||||
).and_return(json.dumps({'archives': [expected_archive]}))
|
||||
|
||||
assert (
|
||||
module.resolve_archive_name(
|
||||
module.get_latest_archive(
|
||||
'repo',
|
||||
'latest',
|
||||
config={'working_directory': '/working/dir'},
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
|
||||
@@ -334,6 +334,16 @@ def test_normalize_commands_moves_individual_command_hooks_to_unified_commands(
|
||||
{'repositories': [{'path': 'rclone:host:repo'}]},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'repositories': ['s3:stuff']},
|
||||
{'repositories': [{'path': 's3:stuff'}]},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'repositories': ['b2:stuff']},
|
||||
{'repositories': [{'path': 'b2:stuff'}]},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'repositories': ['file:///repo']},
|
||||
{'repositories': [{'path': '/repo'}]},
|
||||
|
||||
@@ -5,7 +5,31 @@ from borgmatic.borg.pattern import Pattern, Pattern_source, Pattern_style, Patte
|
||||
from borgmatic.hooks.data_source import btrfs as module
|
||||
|
||||
|
||||
def test_get_subvolume_mount_points_parses_findmnt_output():
|
||||
def test_get_contained_subvolume_paths_parses_btrfs_output():
|
||||
flexmock(module.borgmatic.execute).should_receive(
|
||||
'execute_command_and_capture_output'
|
||||
).with_args(('btrfs', 'subvolume', 'list', '/mnt0'), close_fds=True).and_return(
|
||||
'ID 256 gen 28 top level 5 path @sub\nID 258 gen 17 top level 5 path snap\n\n'
|
||||
)
|
||||
|
||||
assert module.get_contained_subvolume_paths('btrfs', '/mnt0') == (
|
||||
'/mnt0',
|
||||
'/mnt0/@sub',
|
||||
'/mnt0/snap',
|
||||
)
|
||||
|
||||
|
||||
def test_get_contained_subvolume_paths_swallows_called_process_error():
|
||||
flexmock(module.borgmatic.execute).should_receive(
|
||||
'execute_command_and_capture_output'
|
||||
).with_args(('btrfs', 'subvolume', 'list', '/mnt0'), close_fds=True).and_raise(
|
||||
module.subprocess.CalledProcessError(1, 'btrfs')
|
||||
)
|
||||
|
||||
assert module.get_contained_subvolume_paths('btrfs', '/mnt0') == ()
|
||||
|
||||
|
||||
def test_get_all_subvolume_paths_parses_findmnt_output():
|
||||
flexmock(module.borgmatic.execute).should_receive(
|
||||
'execute_command_and_capture_output'
|
||||
).and_return(
|
||||
@@ -22,31 +46,53 @@ def test_get_subvolume_mount_points_parses_findmnt_output():
|
||||
"source": "/dev/loop0",
|
||||
"fstype": "btrfs",
|
||||
"options": "rw,relatime,ssd,space_cache=v2,subvolid=5,subvol=/"
|
||||
},
|
||||
{
|
||||
"target": "/mnt2",
|
||||
"source": "/dev/loop0",
|
||||
"fstype": "btrfs",
|
||||
"options": "rw,relatime,ssd,space_cache=v2,subvolid=256,subvol=/"
|
||||
}
|
||||
]
|
||||
}
|
||||
'''
|
||||
)
|
||||
flexmock(module).should_receive('get_contained_subvolume_paths').with_args(
|
||||
'btrfs', '/mnt0'
|
||||
).and_return(('/mnt0',))
|
||||
flexmock(module).should_receive('get_contained_subvolume_paths').with_args(
|
||||
'btrfs', '/mnt1'
|
||||
).and_return(('/mnt1', '/mnt1/sub'))
|
||||
flexmock(module).should_receive('get_contained_subvolume_paths').with_args(
|
||||
'btrfs', '/mnt2'
|
||||
).never()
|
||||
|
||||
assert module.get_subvolume_mount_points('findmnt') == ('/mnt0', '/mnt1')
|
||||
assert module.get_all_subvolume_paths('btrfs', 'findmnt') == (
|
||||
'/mnt0',
|
||||
'/mnt1',
|
||||
'/mnt1/sub',
|
||||
'/mnt2',
|
||||
)
|
||||
|
||||
|
||||
def test_get_subvolume_mount_points_with_invalid_findmnt_json_errors():
|
||||
def test_get_all_subvolume_paths_with_invalid_findmnt_json_errors():
|
||||
flexmock(module.borgmatic.execute).should_receive(
|
||||
'execute_command_and_capture_output'
|
||||
).and_return('{')
|
||||
flexmock(module).should_receive('get_contained_subvolume_paths').never()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.get_subvolume_mount_points('findmnt')
|
||||
module.get_all_subvolume_paths('btrfs', 'findmnt')
|
||||
|
||||
|
||||
def test_get_subvolume_mount_points_with_findmnt_json_missing_filesystems_errors():
|
||||
def test_get_all_subvolume_paths_with_findmnt_json_missing_filesystems_errors():
|
||||
flexmock(module.borgmatic.execute).should_receive(
|
||||
'execute_command_and_capture_output'
|
||||
).and_return('{"wtf": "something is wrong here"}')
|
||||
flexmock(module).should_receive('get_contained_subvolume_paths').never()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.get_subvolume_mount_points('findmnt')
|
||||
module.get_all_subvolume_paths('btrfs', 'findmnt')
|
||||
|
||||
|
||||
def test_get_subvolume_property_with_invalid_btrfs_output_errors():
|
||||
@@ -82,7 +128,7 @@ def test_get_subvolume_property_passes_through_general_value():
|
||||
assert module.get_subvolume_property('btrfs', '/foo', 'thing') == 'value'
|
||||
|
||||
|
||||
def test_omit_read_only_subvolume_mount_points_filters_out_read_only():
|
||||
def test_omit_read_only_subvolume_paths_filters_out_read_only_subvolumes():
|
||||
flexmock(module).should_receive('get_subvolume_property').with_args(
|
||||
'btrfs', '/foo', 'ro'
|
||||
).and_return(False)
|
||||
@@ -93,17 +139,29 @@ def test_omit_read_only_subvolume_mount_points_filters_out_read_only():
|
||||
'btrfs', '/baz', 'ro'
|
||||
).and_return(False)
|
||||
|
||||
assert module.omit_read_only_subvolume_mount_points('btrfs', ('/foo', '/bar', '/baz')) == (
|
||||
assert module.omit_read_only_subvolume_paths('btrfs', ('/foo', '/bar', '/baz')) == (
|
||||
'/foo',
|
||||
'/baz',
|
||||
)
|
||||
|
||||
|
||||
def test_omit_read_only_subvolume_paths_filters_out_erroring_subvolumes():
|
||||
flexmock(module).should_receive('get_subvolume_property').with_args(
|
||||
'btrfs', '/foo', 'ro'
|
||||
).and_raise(module.subprocess.CalledProcessError(1, 'btrfs'))
|
||||
flexmock(module).should_receive('get_subvolume_property').with_args(
|
||||
'btrfs', '/bar', 'ro'
|
||||
).and_return(True)
|
||||
flexmock(module).should_receive('get_subvolume_property').with_args(
|
||||
'btrfs', '/baz', 'ro'
|
||||
).and_return(False)
|
||||
|
||||
assert module.omit_read_only_subvolume_paths('btrfs', ('/foo', '/bar', '/baz')) == ('/baz',)
|
||||
|
||||
|
||||
def test_get_subvolumes_collects_subvolumes_matching_patterns():
|
||||
flexmock(module).should_receive('get_subvolume_mount_points').and_return(('/mnt1', '/mnt2'))
|
||||
flexmock(module).should_receive('omit_read_only_subvolume_mount_points').and_return(
|
||||
('/mnt1', '/mnt2')
|
||||
)
|
||||
flexmock(module).should_receive('get_all_subvolume_paths').and_return(('/mnt1', '/mnt2'))
|
||||
flexmock(module).should_receive('omit_read_only_subvolume_paths').and_return(('/mnt1', '/mnt2'))
|
||||
|
||||
contained_pattern = Pattern(
|
||||
'/mnt1',
|
||||
@@ -128,10 +186,8 @@ def test_get_subvolumes_collects_subvolumes_matching_patterns():
|
||||
|
||||
|
||||
def test_get_subvolumes_skips_non_root_patterns():
|
||||
flexmock(module).should_receive('get_subvolume_mount_points').and_return(('/mnt1', '/mnt2'))
|
||||
flexmock(module).should_receive('omit_read_only_subvolume_mount_points').and_return(
|
||||
('/mnt1', '/mnt2')
|
||||
)
|
||||
flexmock(module).should_receive('get_all_subvolume_paths').and_return(('/mnt1', '/mnt2'))
|
||||
flexmock(module).should_receive('omit_read_only_subvolume_paths').and_return(('/mnt1', '/mnt2'))
|
||||
|
||||
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
|
||||
'get_contained_patterns'
|
||||
@@ -162,10 +218,8 @@ def test_get_subvolumes_skips_non_root_patterns():
|
||||
|
||||
|
||||
def test_get_subvolumes_skips_non_config_patterns():
|
||||
flexmock(module).should_receive('get_subvolume_mount_points').and_return(('/mnt1', '/mnt2'))
|
||||
flexmock(module).should_receive('omit_read_only_subvolume_mount_points').and_return(
|
||||
('/mnt1', '/mnt2')
|
||||
)
|
||||
flexmock(module).should_receive('get_all_subvolume_paths').and_return(('/mnt1', '/mnt2'))
|
||||
flexmock(module).should_receive('omit_read_only_subvolume_paths').and_return(('/mnt1', '/mnt2'))
|
||||
|
||||
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
|
||||
'get_contained_patterns'
|
||||
@@ -196,10 +250,8 @@ def test_get_subvolumes_skips_non_config_patterns():
|
||||
|
||||
|
||||
def test_get_subvolumes_without_patterns_collects_all_subvolumes():
|
||||
flexmock(module).should_receive('get_subvolume_mount_points').and_return(('/mnt1', '/mnt2'))
|
||||
flexmock(module).should_receive('omit_read_only_subvolume_mount_points').and_return(
|
||||
('/mnt1', '/mnt2')
|
||||
)
|
||||
flexmock(module).should_receive('get_all_subvolume_paths').and_return(('/mnt1', '/mnt2'))
|
||||
flexmock(module).should_receive('omit_read_only_subvolume_paths').and_return(('/mnt1', '/mnt2'))
|
||||
|
||||
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
|
||||
'get_contained_patterns'
|
||||
|
||||
@@ -214,6 +214,7 @@ def test_snapshot_logical_volume_with_percentage_snapshot_name_uses_lvcreate_ext
|
||||
'/dev/snap',
|
||||
),
|
||||
output_log_level=object,
|
||||
close_fds=True,
|
||||
)
|
||||
|
||||
module.snapshot_logical_volume('lvcreate', 'snap', '/dev/snap', '10%ORIGIN')
|
||||
@@ -233,6 +234,7 @@ def test_snapshot_logical_volume_with_non_percentage_snapshot_name_uses_lvcreate
|
||||
'/dev/snap',
|
||||
),
|
||||
output_log_level=object,
|
||||
close_fds=True,
|
||||
)
|
||||
|
||||
module.snapshot_logical_volume('lvcreate', 'snap', '/dev/snap', '10TB')
|
||||
@@ -554,16 +556,7 @@ def test_dump_data_sources_with_dry_run_skips_snapshots_and_does_not_touch_patte
|
||||
)
|
||||
flexmock(module.os).should_receive('getpid').and_return(1234)
|
||||
flexmock(module).should_receive('snapshot_logical_volume').never()
|
||||
flexmock(module).should_receive('get_snapshots').with_args(
|
||||
'lvs', snapshot_name='lvolume1_borgmatic-1234'
|
||||
).and_return(
|
||||
(module.Snapshot(name='lvolume1_borgmatic-1234', device_path='/dev/lvolume1_snap'),)
|
||||
)
|
||||
flexmock(module).should_receive('get_snapshots').with_args(
|
||||
'lvs', snapshot_name='lvolume2_borgmatic-1234'
|
||||
).and_return(
|
||||
(module.Snapshot(name='lvolume2_borgmatic-1234', device_path='/dev/lvolume2_snap'),)
|
||||
)
|
||||
flexmock(module).should_receive('get_snapshots').never()
|
||||
flexmock(module).should_receive('mount_snapshot').never()
|
||||
|
||||
assert (
|
||||
|
||||
@@ -6,7 +6,7 @@ from borgmatic.hooks.monitoring import cronhub as module
|
||||
def test_ping_monitor_rewrites_ping_url_for_start_state():
|
||||
hook_config = {'ping_url': 'https://example.com/start/abcdef'}
|
||||
flexmock(module.requests).should_receive('get').with_args(
|
||||
'https://example.com/start/abcdef'
|
||||
'https://example.com/start/abcdef', timeout=int
|
||||
).and_return(flexmock(ok=True))
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -22,7 +22,7 @@ def test_ping_monitor_rewrites_ping_url_for_start_state():
|
||||
def test_ping_monitor_rewrites_ping_url_and_state_for_start_state():
|
||||
hook_config = {'ping_url': 'https://example.com/ping/abcdef'}
|
||||
flexmock(module.requests).should_receive('get').with_args(
|
||||
'https://example.com/start/abcdef'
|
||||
'https://example.com/start/abcdef', timeout=int
|
||||
).and_return(flexmock(ok=True))
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -38,7 +38,7 @@ def test_ping_monitor_rewrites_ping_url_and_state_for_start_state():
|
||||
def test_ping_monitor_rewrites_ping_url_for_finish_state():
|
||||
hook_config = {'ping_url': 'https://example.com/start/abcdef'}
|
||||
flexmock(module.requests).should_receive('get').with_args(
|
||||
'https://example.com/finish/abcdef'
|
||||
'https://example.com/finish/abcdef', timeout=int
|
||||
).and_return(flexmock(ok=True))
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -54,7 +54,7 @@ def test_ping_monitor_rewrites_ping_url_for_finish_state():
|
||||
def test_ping_monitor_rewrites_ping_url_for_fail_state():
|
||||
hook_config = {'ping_url': 'https://example.com/start/abcdef'}
|
||||
flexmock(module.requests).should_receive('get').with_args(
|
||||
'https://example.com/fail/abcdef'
|
||||
'https://example.com/fail/abcdef', timeout=int
|
||||
).and_return(flexmock(ok=True))
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -105,7 +105,7 @@ def test_ping_monitor_with_other_error_logs_warning():
|
||||
module.requests.exceptions.RequestException
|
||||
)
|
||||
flexmock(module.requests).should_receive('get').with_args(
|
||||
'https://example.com/start/abcdef'
|
||||
'https://example.com/start/abcdef', timeout=int
|
||||
).and_return(response)
|
||||
flexmock(module.logger).should_receive('warning').once()
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ from borgmatic.hooks.monitoring import cronitor as module
|
||||
|
||||
def test_ping_monitor_hits_ping_url_for_start_state():
|
||||
hook_config = {'ping_url': 'https://example.com'}
|
||||
flexmock(module.requests).should_receive('get').with_args('https://example.com/run').and_return(
|
||||
flexmock(ok=True)
|
||||
)
|
||||
flexmock(module.requests).should_receive('get').with_args(
|
||||
'https://example.com/run', timeout=int
|
||||
).and_return(flexmock(ok=True))
|
||||
|
||||
module.ping_monitor(
|
||||
hook_config,
|
||||
@@ -22,7 +22,7 @@ def test_ping_monitor_hits_ping_url_for_start_state():
|
||||
def test_ping_monitor_hits_ping_url_for_finish_state():
|
||||
hook_config = {'ping_url': 'https://example.com'}
|
||||
flexmock(module.requests).should_receive('get').with_args(
|
||||
'https://example.com/complete'
|
||||
'https://example.com/complete', timeout=int
|
||||
).and_return(flexmock(ok=True))
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -38,7 +38,7 @@ def test_ping_monitor_hits_ping_url_for_finish_state():
|
||||
def test_ping_monitor_hits_ping_url_for_fail_state():
|
||||
hook_config = {'ping_url': 'https://example.com'}
|
||||
flexmock(module.requests).should_receive('get').with_args(
|
||||
'https://example.com/fail'
|
||||
'https://example.com/fail', timeout=int
|
||||
).and_return(flexmock(ok=True))
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -88,9 +88,9 @@ def test_ping_monitor_with_other_error_logs_warning():
|
||||
response.should_receive('raise_for_status').and_raise(
|
||||
module.requests.exceptions.RequestException
|
||||
)
|
||||
flexmock(module.requests).should_receive('get').with_args('https://example.com/run').and_return(
|
||||
response
|
||||
)
|
||||
flexmock(module.requests).should_receive('get').with_args(
|
||||
'https://example.com/run', timeout=int
|
||||
).and_return(response)
|
||||
flexmock(module.logger).should_receive('warning').once()
|
||||
|
||||
module.ping_monitor(
|
||||
|
||||
@@ -79,7 +79,7 @@ def test_ping_monitor_hits_ping_url_for_start_state():
|
||||
).never()
|
||||
hook_config = {'ping_url': 'https://example.com'}
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'https://example.com/start', data=''.encode('utf-8'), verify=True
|
||||
'https://example.com/start', data=''.encode('utf-8'), verify=True, timeout=int
|
||||
).and_return(flexmock(ok=True))
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -100,7 +100,7 @@ def test_ping_monitor_hits_ping_url_for_finish_state():
|
||||
'format_buffered_logs_for_payload'
|
||||
).and_return(payload)
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'https://example.com', data=payload.encode('utf-8'), verify=True
|
||||
'https://example.com', data=payload.encode('utf-8'), verify=True, timeout=int
|
||||
).and_return(flexmock(ok=True))
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -121,7 +121,7 @@ def test_ping_monitor_hits_ping_url_for_fail_state():
|
||||
'format_buffered_logs_for_payload'
|
||||
).and_return(payload)
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'https://example.com/fail', data=payload.encode('utf'), verify=True
|
||||
'https://example.com/fail', data=payload.encode('utf'), verify=True, timeout=int
|
||||
).and_return(flexmock(ok=True))
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -142,7 +142,7 @@ def test_ping_monitor_hits_ping_url_for_log_state():
|
||||
'format_buffered_logs_for_payload'
|
||||
).and_return(payload)
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'https://example.com/log', data=payload.encode('utf'), verify=True
|
||||
'https://example.com/log', data=payload.encode('utf'), verify=True, timeout=int
|
||||
).and_return(flexmock(ok=True))
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -166,6 +166,7 @@ def test_ping_monitor_with_ping_uuid_hits_corresponding_url():
|
||||
f"https://hc-ping.com/{hook_config['ping_url']}",
|
||||
data=payload.encode('utf-8'),
|
||||
verify=True,
|
||||
timeout=int,
|
||||
).and_return(flexmock(ok=True))
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -186,7 +187,7 @@ def test_ping_monitor_skips_ssl_verification_when_verify_tls_false():
|
||||
'format_buffered_logs_for_payload'
|
||||
).and_return(payload)
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'https://example.com', data=payload.encode('utf-8'), verify=False
|
||||
'https://example.com', data=payload.encode('utf-8'), verify=False, timeout=int
|
||||
).and_return(flexmock(ok=True))
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -207,7 +208,7 @@ def test_ping_monitor_executes_ssl_verification_when_verify_tls_true():
|
||||
'format_buffered_logs_for_payload'
|
||||
).and_return(payload)
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'https://example.com', data=payload.encode('utf-8'), verify=True
|
||||
'https://example.com', data=payload.encode('utf-8'), verify=True, timeout=int
|
||||
).and_return(flexmock(ok=True))
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -260,7 +261,7 @@ def test_ping_monitor_hits_ping_url_when_states_matching():
|
||||
).never()
|
||||
hook_config = {'ping_url': 'https://example.com', 'states': ['start', 'finish']}
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'https://example.com/start', data=''.encode('utf-8'), verify=True
|
||||
'https://example.com/start', data=''.encode('utf-8'), verify=True, timeout=int
|
||||
).and_return(flexmock(ok=True))
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -279,7 +280,7 @@ def test_ping_monitor_adds_create_query_parameter_when_create_slug_true():
|
||||
).never()
|
||||
hook_config = {'ping_url': 'https://example.com', 'create_slug': True}
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'https://example.com/start?create=1', data=''.encode('utf-8'), verify=True
|
||||
'https://example.com/start?create=1', data=''.encode('utf-8'), verify=True, timeout=int
|
||||
).and_return(flexmock(ok=True))
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -298,7 +299,7 @@ def test_ping_monitor_does_not_add_create_query_parameter_when_create_slug_false
|
||||
).never()
|
||||
hook_config = {'ping_url': 'https://example.com', 'create_slug': False}
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'https://example.com/start', data=''.encode('utf-8'), verify=True
|
||||
'https://example.com/start', data=''.encode('utf-8'), verify=True, timeout=int
|
||||
).and_return(flexmock(ok=True))
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -317,6 +318,7 @@ def test_ping_monitor_does_not_add_create_query_parameter_when_ping_url_is_uuid(
|
||||
f"https://hc-ping.com/{hook_config['ping_url']}",
|
||||
data=''.encode('utf-8'),
|
||||
verify=True,
|
||||
timeout=int,
|
||||
).and_return(flexmock(ok=True))
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -352,7 +354,7 @@ def test_ping_monitor_with_connection_error_logs_warning():
|
||||
).never()
|
||||
hook_config = {'ping_url': 'https://example.com'}
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'https://example.com/start', data=''.encode('utf-8'), verify=True
|
||||
'https://example.com/start', data=''.encode('utf-8'), verify=True, timeout=int
|
||||
).and_raise(module.requests.exceptions.ConnectionError)
|
||||
flexmock(module.logger).should_receive('warning').once()
|
||||
|
||||
@@ -376,7 +378,7 @@ def test_ping_monitor_with_other_error_logs_warning():
|
||||
module.requests.exceptions.RequestException
|
||||
)
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'https://example.com/start', data=''.encode('utf-8'), verify=True
|
||||
'https://example.com/start', data=''.encode('utf-8'), verify=True, timeout=int
|
||||
).and_return(response)
|
||||
flexmock(module.logger).should_receive('warning').once()
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ def test_ping_monitor_minimal_config_hits_hosted_ntfy_on_fail():
|
||||
f'{default_base_url}/{topic}',
|
||||
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
|
||||
auth=None,
|
||||
timeout=int,
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -67,6 +68,7 @@ def test_ping_monitor_with_access_token_hits_hosted_ntfy_on_fail():
|
||||
f'{default_base_url}/{topic}',
|
||||
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
|
||||
auth=module.requests.auth.HTTPBasicAuth('', 'abc123'),
|
||||
timeout=int,
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -93,6 +95,7 @@ def test_ping_monitor_with_username_password_and_access_token_ignores_username_p
|
||||
f'{default_base_url}/{topic}',
|
||||
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
|
||||
auth=module.requests.auth.HTTPBasicAuth('', 'abc123'),
|
||||
timeout=int,
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
flexmock(module.logger).should_receive('warning').once()
|
||||
|
||||
@@ -119,6 +122,7 @@ def test_ping_monitor_with_username_password_hits_hosted_ntfy_on_fail():
|
||||
f'{default_base_url}/{topic}',
|
||||
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
|
||||
auth=module.requests.auth.HTTPBasicAuth('testuser', 'fakepassword'),
|
||||
timeout=int,
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -140,6 +144,7 @@ def test_ping_monitor_with_password_but_no_username_warns():
|
||||
f'{default_base_url}/{topic}',
|
||||
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
|
||||
auth=None,
|
||||
timeout=int,
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
flexmock(module.logger).should_receive('warning').once()
|
||||
|
||||
@@ -162,6 +167,7 @@ def test_ping_monitor_with_username_but_no_password_warns():
|
||||
f'{default_base_url}/{topic}',
|
||||
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
|
||||
auth=None,
|
||||
timeout=int,
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
flexmock(module.logger).should_receive('warning').once()
|
||||
|
||||
@@ -218,6 +224,7 @@ def test_ping_monitor_minimal_config_hits_selfhosted_ntfy_on_fail():
|
||||
f'{custom_base_url}/{topic}',
|
||||
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
|
||||
auth=None,
|
||||
timeout=int,
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -253,7 +260,7 @@ def test_ping_monitor_custom_message_hits_hosted_ntfy_on_fail():
|
||||
'resolve_credential'
|
||||
).replace_with(lambda value, config: value)
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
f'{default_base_url}/{topic}', headers=custom_message_headers, auth=None
|
||||
f'{default_base_url}/{topic}', headers=custom_message_headers, auth=None, timeout=int
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -275,6 +282,7 @@ def test_ping_monitor_custom_state_hits_hosted_ntfy_on_start():
|
||||
f'{default_base_url}/{topic}',
|
||||
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.START),
|
||||
auth=None,
|
||||
timeout=int,
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -296,6 +304,7 @@ def test_ping_monitor_with_connection_error_logs_warning():
|
||||
f'{default_base_url}/{topic}',
|
||||
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
|
||||
auth=None,
|
||||
timeout=int,
|
||||
).and_raise(module.requests.exceptions.ConnectionError)
|
||||
flexmock(module.logger).should_receive('warning').once()
|
||||
|
||||
@@ -340,6 +349,7 @@ def test_ping_monitor_with_other_error_logs_warning():
|
||||
f'{default_base_url}/{topic}',
|
||||
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
|
||||
auth=None,
|
||||
timeout=int,
|
||||
).and_return(response)
|
||||
flexmock(module.logger).should_receive('warning').once()
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ def test_ping_monitor_config_with_minimum_config_fail_state_backup_successfully_
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
'message': 'fail',
|
||||
},
|
||||
timeout=int,
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -81,6 +82,7 @@ def test_ping_monitor_start_state_backup_default_message_successfully_send_to_pu
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
'message': 'start',
|
||||
},
|
||||
timeout=int,
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -117,6 +119,7 @@ def test_ping_monitor_start_state_backup_custom_message_successfully_send_to_pus
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
'message': 'custom start message',
|
||||
},
|
||||
timeout=int,
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -155,6 +158,7 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_emergency
|
||||
'retry': 30,
|
||||
'expire': 600,
|
||||
},
|
||||
timeout=int,
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -193,6 +197,7 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_emergency
|
||||
'retry': 30,
|
||||
'expire': 600,
|
||||
},
|
||||
timeout=int,
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -231,6 +236,7 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_emergency
|
||||
'retry': 30,
|
||||
'expire': 600,
|
||||
},
|
||||
timeout=int,
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -328,6 +334,7 @@ def test_ping_monitor_start_state_backup_based_on_documentation_advanced_example
|
||||
'html': 1,
|
||||
'ttl': 10,
|
||||
},
|
||||
timeout=int,
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -399,6 +406,7 @@ def test_ping_monitor_fail_state_backup_based_on_documentation_advanced_example_
|
||||
'url': 'https://ticketing-system.example.com/login',
|
||||
'url_title': 'Login to ticketing system',
|
||||
},
|
||||
timeout=int,
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -467,6 +475,7 @@ def test_ping_monitor_finish_state_backup_based_on_documentation_advanced_exampl
|
||||
'url': 'https://ticketing-system.example.com/login',
|
||||
'url_title': 'Login to ticketing system',
|
||||
},
|
||||
timeout=int,
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -550,6 +559,7 @@ def test_ping_monitor_push_post_error_bails():
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
'message': 'fail',
|
||||
},
|
||||
timeout=int,
|
||||
).and_return(push_response).once()
|
||||
|
||||
flexmock(module.logger).should_receive('warning').once()
|
||||
|
||||
@@ -29,7 +29,8 @@ def test_ping_monitor_constructs_cron_url_and_pings_it(state, configured_states,
|
||||
hook_config['states'] = configured_states
|
||||
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
f'https://o294220.ingest.us.sentry.io/api/203069/cron/test/5f80ec/?status={expected_status}'
|
||||
f'https://o294220.ingest.us.sentry.io/api/203069/cron/test/5f80ec/?status={expected_status}',
|
||||
timeout=int,
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -134,7 +135,8 @@ def test_ping_monitor_with_network_error_does_not_raise():
|
||||
module.requests.exceptions.ConnectionError
|
||||
)
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'https://o294220.ingest.us.sentry.io/api/203069/cron/test/5f80ec/?status=in_progress'
|
||||
'https://o294220.ingest.us.sentry.io/api/203069/cron/test/5f80ec/?status=in_progress',
|
||||
timeout=int,
|
||||
).and_return(response).once()
|
||||
|
||||
module.ping_monitor(
|
||||
|
||||
@@ -10,7 +10,7 @@ CUSTOM_PUSH_URL = 'https://uptime.example.com/api/push/efgh5678'
|
||||
def test_ping_monitor_hits_default_uptimekuma_on_fail():
|
||||
hook_config = {}
|
||||
flexmock(module.requests).should_receive('get').with_args(
|
||||
f'{DEFAULT_PUSH_URL}?status=down&msg=fail', verify=True
|
||||
f'{DEFAULT_PUSH_URL}?status=down&msg=fail', verify=True, timeout=int
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -26,7 +26,7 @@ def test_ping_monitor_hits_default_uptimekuma_on_fail():
|
||||
def test_ping_monitor_hits_custom_uptimekuma_on_fail():
|
||||
hook_config = {'push_url': CUSTOM_PUSH_URL}
|
||||
flexmock(module.requests).should_receive('get').with_args(
|
||||
f'{CUSTOM_PUSH_URL}?status=down&msg=fail', verify=True
|
||||
f'{CUSTOM_PUSH_URL}?status=down&msg=fail', verify=True, timeout=int
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -42,7 +42,7 @@ def test_ping_monitor_hits_custom_uptimekuma_on_fail():
|
||||
def test_ping_monitor_custom_uptimekuma_on_start():
|
||||
hook_config = {'push_url': CUSTOM_PUSH_URL}
|
||||
flexmock(module.requests).should_receive('get').with_args(
|
||||
f'{CUSTOM_PUSH_URL}?status=up&msg=start', verify=True
|
||||
f'{CUSTOM_PUSH_URL}?status=up&msg=start', verify=True, timeout=int
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -58,7 +58,7 @@ def test_ping_monitor_custom_uptimekuma_on_start():
|
||||
def test_ping_monitor_custom_uptimekuma_on_finish():
|
||||
hook_config = {'push_url': CUSTOM_PUSH_URL}
|
||||
flexmock(module.requests).should_receive('get').with_args(
|
||||
f'{CUSTOM_PUSH_URL}?status=up&msg=finish', verify=True
|
||||
f'{CUSTOM_PUSH_URL}?status=up&msg=finish', verify=True, timeout=int
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -116,7 +116,7 @@ def test_ping_monitor_does_not_hit_custom_uptimekuma_on_finish_dry_run():
|
||||
def test_ping_monitor_with_connection_error_logs_warning():
|
||||
hook_config = {'push_url': CUSTOM_PUSH_URL}
|
||||
flexmock(module.requests).should_receive('get').with_args(
|
||||
f'{CUSTOM_PUSH_URL}?status=down&msg=fail', verify=True
|
||||
f'{CUSTOM_PUSH_URL}?status=down&msg=fail', verify=True, timeout=int
|
||||
).and_raise(module.requests.exceptions.ConnectionError)
|
||||
flexmock(module.logger).should_receive('warning').once()
|
||||
|
||||
@@ -137,7 +137,7 @@ def test_ping_monitor_with_other_error_logs_warning():
|
||||
module.requests.exceptions.RequestException
|
||||
)
|
||||
flexmock(module.requests).should_receive('get').with_args(
|
||||
f'{CUSTOM_PUSH_URL}?status=down&msg=fail', verify=True
|
||||
f'{CUSTOM_PUSH_URL}?status=down&msg=fail', verify=True, timeout=int
|
||||
).and_return(response)
|
||||
flexmock(module.logger).should_receive('warning').once()
|
||||
|
||||
@@ -168,7 +168,7 @@ def test_ping_monitor_with_invalid_run_state():
|
||||
def test_ping_monitor_skips_ssl_verification_when_verify_tls_false():
|
||||
hook_config = {'push_url': CUSTOM_PUSH_URL, 'verify_tls': False}
|
||||
flexmock(module.requests).should_receive('get').with_args(
|
||||
f'{CUSTOM_PUSH_URL}?status=down&msg=fail', verify=False
|
||||
f'{CUSTOM_PUSH_URL}?status=down&msg=fail', verify=False, timeout=int
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -184,7 +184,7 @@ def test_ping_monitor_skips_ssl_verification_when_verify_tls_false():
|
||||
def test_ping_monitor_executes_ssl_verification_when_verify_tls_true():
|
||||
hook_config = {'push_url': CUSTOM_PUSH_URL, 'verify_tls': True}
|
||||
flexmock(module.requests).should_receive('get').with_args(
|
||||
f'{CUSTOM_PUSH_URL}?status=down&msg=fail', verify=True
|
||||
f'{CUSTOM_PUSH_URL}?status=down&msg=fail', verify=True, timeout=int
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
|
||||
@@ -74,7 +74,7 @@ def test_send_zabbix_request_with_post_error_bails():
|
||||
)
|
||||
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
server, headers=headers, json=data
|
||||
server, headers=headers, json=data, timeout=int
|
||||
).and_return(response)
|
||||
|
||||
assert module.send_zabbix_request(server, headers, data) is None
|
||||
@@ -89,7 +89,7 @@ def test_send_zabbix_request_with_invalid_json_response_bails():
|
||||
response.should_receive('json').and_raise(module.requests.exceptions.JSONDecodeError)
|
||||
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
server, headers=headers, json=data
|
||||
server, headers=headers, json=data, timeout=int
|
||||
).and_return(response)
|
||||
|
||||
assert module.send_zabbix_request(server, headers, data) is None
|
||||
@@ -103,7 +103,7 @@ def test_send_zabbix_request_with_success_returns_response_result():
|
||||
response.should_receive('json').and_return({'result': {'foo': 'bar'}})
|
||||
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
server, headers=headers, json=data
|
||||
server, headers=headers, json=data, timeout=int
|
||||
).and_return(response)
|
||||
|
||||
assert module.send_zabbix_request(server, headers, data) == {'foo': 'bar'}
|
||||
@@ -117,7 +117,7 @@ def test_send_zabbix_request_with_success_passes_through_missing_result():
|
||||
response.should_receive('json').and_return({})
|
||||
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
server, headers=headers, json=data
|
||||
server, headers=headers, json=data, timeout=int
|
||||
).and_return(response)
|
||||
|
||||
assert module.send_zabbix_request(server, headers, data) is None
|
||||
@@ -131,7 +131,7 @@ def test_send_zabbix_request_with_error_bails():
|
||||
response.should_receive('json').and_return({'result': {'data': [{'error': 'oops'}]}})
|
||||
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
server, headers=headers, json=data
|
||||
server, headers=headers, json=data, timeout=int
|
||||
).and_return(response)
|
||||
|
||||
assert module.send_zabbix_request(server, headers, data) is None
|
||||
|
||||
Reference in New Issue
Block a user