mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-24 02:43:02 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82141fe981 | ||
|
|
228a83978d | ||
|
|
638db3770b | ||
|
|
98df5c3af2 | ||
|
|
b0e906c0e7 | ||
|
|
e8dccbf1c1 | ||
|
|
4a997bc234 | ||
|
|
3197178b3d | ||
|
|
5e618154d0 | ||
|
|
84f611ae4f | ||
|
|
5dc8450c8e | ||
|
|
689643e5fa | ||
|
|
0a3d87eaea | ||
|
|
b45b62cd38 | ||
|
|
8de7094691 | ||
|
|
8c7e68305e | ||
|
|
65a323433c | ||
|
|
b5a3589471 | ||
|
|
f4a736bdfe | ||
|
|
eab0ec15ef | ||
|
|
c65aa24001 | ||
|
|
5a24bf2037 | ||
|
|
324dbc3a79 | ||
|
|
9fe7db320a | ||
|
|
4d19596616 | ||
|
|
5cec2bf3d9 | ||
|
|
06e0f98fd8 | ||
|
|
87f36caf8d | ||
|
|
ab7acceff6 | ||
|
|
1b2b0c3020 | ||
|
|
289d178581 | ||
|
|
1e7f6d9f41 | ||
|
|
d0c90389fb | ||
|
|
f9e920dce9 | ||
|
|
0ed52bbc4a | ||
|
|
da8278b566 | ||
|
|
2af3522902 | ||
|
|
5e4784991a | ||
|
|
ab43ef00ce | ||
|
|
47a8a95b29 | ||
|
|
7c90c04ce0 | ||
|
|
97305cc3ce | ||
|
|
4985b805b4 | ||
|
|
d09b4c72a9 | ||
|
|
9807549f88 | ||
|
|
30c821120e | ||
|
|
13884bd448 | ||
|
|
6bce4c4a0d | ||
|
|
25572c98d7 | ||
|
|
dab0dfcb32 | ||
|
|
851c454ef0 | ||
|
|
c7a0cebaf7 | ||
|
|
76cfeda290 | ||
|
|
afdf831c59 | ||
|
|
9ac3087304 | ||
|
|
7cca83b698 | ||
|
|
4b5df7117a | ||
|
|
57decfa4db | ||
|
|
b80f60a731 | ||
|
|
8f5ea95348 | ||
|
|
b0cad58d6c | ||
|
|
073d6bddf6 | ||
|
|
810b65589f | ||
|
|
295bfb0c57 | ||
|
|
5f3d4f9b03 | ||
|
|
5321301708 | ||
|
|
a939a66fb4 | ||
|
|
c0721a8cad | ||
|
|
ea47704d86 | ||
|
|
61e4eeff6c | ||
|
|
3ab4b45041 | ||
|
|
4e1f256b48 | ||
|
|
96bb402837 | ||
|
|
97949266b3 | ||
|
|
e69d2385fc | ||
|
|
6d9340ebb2 | ||
|
|
0441e79b41 | ||
|
|
b1af304125 | ||
|
|
eb8f7e0329 | ||
|
|
bf978f2db4 | ||
|
|
22776b123d | ||
|
|
ef66349674 | ||
|
|
51b885e7db | ||
|
|
1781787305 | ||
|
|
46ebb0cebb | ||
|
|
3e0fa57860 | ||
|
|
59f8722e05 | ||
|
|
4ba42e8905 | ||
|
|
3b79482b24 | ||
|
|
7eb19cb0a7 | ||
|
|
a4fabb8521 | ||
|
|
85ea8f4f45 | ||
|
|
290559116d | ||
|
|
72b27b0858 | ||
|
|
0fdee067c7 | ||
|
|
0dca5eeafc | ||
|
|
02ce3ba190 | ||
|
|
dc78bf4d6b | ||
|
|
4b7fbce291 | ||
|
|
1817b9a9ea | ||
|
|
009055c61a | ||
|
|
54884da8fa | ||
|
|
1177385e08 | ||
|
|
a45ba8553c | ||
|
|
d7d6e30178 | ||
|
|
56304fdcad | ||
|
|
3f75e9931f | ||
|
|
227f475e17 | ||
|
|
467ddd0e93 | ||
|
|
be08e889f0 | ||
|
|
94c8a56373 | ||
|
|
94db527500 | ||
|
|
2849f54932 |
@@ -1,3 +1,37 @@
|
||||
1.9.3
|
||||
* #261 (beta): Add a ZFS hook for snapshotting and backing up ZFS datasets. See the documentation
|
||||
for more information: https://torsion.org/borgmatic/docs/how-to/snapshot-your-filesystems/
|
||||
* Remove any temporary copies of the manifest file created in support of the "bootstrap" action.
|
||||
* Deprecate the "store_config_files" option at the global scope and move it under the "bootstrap"
|
||||
hook. See the documentation for more information:
|
||||
https://torsion.org/borgmatic/docs/how-to/extract-a-backup/#extract-the-configuration-files-used-to-create-an-archive
|
||||
* Require the runtime directory to be an absolute path.
|
||||
* Add a "--deleted" flag to the "repo-list" action for listing deleted archives that haven't
|
||||
yet been compacted (Borg 2 only).
|
||||
* Promote the "spot" check from a beta feature to stable.
|
||||
|
||||
1.9.2
|
||||
* #441: Apply the "umask" option to all relevant actions, not just some of them.
|
||||
* #722: Remove the restriction that the "extract" and "mount" actions must match a single
|
||||
repository. Now they work more like other actions, where each repository is applied in turn.
|
||||
* #932: Fix the missing build backend setting in pyproject.toml to allow Fedora builds.
|
||||
* #934: Update the logic that probes for the borgmatic streaming database dump, bootstrap
|
||||
metadata, and check state directories to support more platforms and use cases. See the
|
||||
documentation for more information:
|
||||
https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#runtime-directory
|
||||
* #934: Add the "RuntimeDirectory" and "StateDirectory" options to the sample systemd service
|
||||
file to support the new runtime and state directory logic.
|
||||
* #939: Fix borgmatic ignoring the "BORG_RELOCATED_REPO_ACCESS_IS_OK" and
|
||||
"BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK" environment variables.
|
||||
* Add a Pushover monitoring hook. See the documentation for more information:
|
||||
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pushover-hook
|
||||
|
||||
1.9.1
|
||||
* #928: Fix the user runtime directory location on macOS (and possibly Cygwin).
|
||||
* #930: Fix an error with the sample systemd service when no credentials are configured.
|
||||
* #931: Fix an error when implicitly upgrading the check state directory from ~/.borgmatic to
|
||||
~/.local/state/borgmatic across filesystems.
|
||||
|
||||
1.9.0
|
||||
* #609: Fix the glob expansion of "source_directories" values to respect the "working_directory"
|
||||
option.
|
||||
@@ -20,6 +54,7 @@
|
||||
cases.
|
||||
* #902: Add loading of encrypted systemd credentials. See the documentation for more information:
|
||||
https://torsion.org/borgmatic/docs/how-to/provide-your-passwords/#using-systemd-service-credentials
|
||||
* #911: Add a "key change-passphrase" action to change the passphrase protecting a repository key.
|
||||
* #914: Fix a confusing apparent hang when when the repository location changes, and instead
|
||||
show a helpful error message.
|
||||
* #915: BREAKING: Rename repository actions like "rcreate" to more explicit names like
|
||||
@@ -31,7 +66,6 @@
|
||||
* #919: Clarify the command-line help for the "--config" flag.
|
||||
* #919: Document a policy for versioning and breaking changes:
|
||||
https://torsion.org/borgmatic/docs/how-to/upgrade/#versioning-and-breaking-changes
|
||||
* #911: Add a "key change-passphrase" action to change the passphrase protecting a repository key.
|
||||
* #921: BREAKING: Change soft failure command hooks to skip only the current repository rather than
|
||||
all repositories in the configuration file.
|
||||
* #922: Replace setup.py (Python packaging metadata) with the more modern pyproject.toml.
|
||||
|
||||
@@ -61,11 +61,14 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
|
||||
<a href="https://mariadb.com/"><img src="docs/static/mariadb.png" alt="MariaDB" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://www.mongodb.com/"><img src="docs/static/mongodb.png" alt="MongoDB" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://sqlite.org/"><img src="docs/static/sqlite.png" alt="SQLite" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://openzfs.org/"><img src="docs/static/openzfs.png" alt="OpenZFS" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://rclone.org"><img src="docs/static/rclone.png" alt="rclone" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://healthchecks.io/"><img src="docs/static/healthchecks.png" alt="Healthchecks" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://uptime.kuma.pet/"><img src="docs/static/uptimekuma.png" alt="Uptime Kuma" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://cronitor.io/"><img src="docs/static/cronitor.png" alt="Cronitor" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://cronhub.io/"><img src="docs/static/cronhub.png" alt="Cronhub" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://www.pagerduty.com/"><img src="docs/static/pagerduty.png" alt="PagerDuty" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://www.pushover.net/"><img src="docs/static/pushover.png" alt="Pushover" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://ntfy.sh/"><img src="docs/static/ntfy.png" alt="ntfy" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://grafana.com/oss/loki/"><img src="docs/static/loki.png" alt="Loki" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://github.com/caronc/apprise/wiki"><img src="docs/static/apprise.png" alt="Apprise" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
|
||||
+60
-35
@@ -6,7 +6,9 @@ import logging
|
||||
import os
|
||||
import pathlib
|
||||
import random
|
||||
import shutil
|
||||
|
||||
import borgmatic.actions.create
|
||||
import borgmatic.borg.check
|
||||
import borgmatic.borg.create
|
||||
import borgmatic.borg.environment
|
||||
@@ -322,7 +324,7 @@ def upgrade_check_times(config, borg_repository_id):
|
||||
f'Upgrading archives check times directory from {borgmatic_source_checks_path} to {borgmatic_state_checks_path}'
|
||||
)
|
||||
os.makedirs(borgmatic_state_path, mode=0o700, exist_ok=True)
|
||||
os.rename(borgmatic_source_checks_path, borgmatic_state_checks_path)
|
||||
shutil.move(borgmatic_source_checks_path, borgmatic_state_checks_path)
|
||||
|
||||
for check_type in ('archives', 'data'):
|
||||
new_path = make_check_time_path(config, borg_repository_id, check_type, 'all')
|
||||
@@ -335,16 +337,22 @@ def upgrade_check_times(config, borg_repository_id):
|
||||
logger.debug(f'Upgrading archives check time file from {old_path} to {new_path}')
|
||||
|
||||
try:
|
||||
os.rename(old_path, temporary_path)
|
||||
shutil.move(old_path, temporary_path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
os.mkdir(old_path)
|
||||
os.rename(temporary_path, new_path)
|
||||
shutil.move(temporary_path, new_path)
|
||||
|
||||
|
||||
def collect_spot_check_source_paths(
|
||||
repository, config, local_borg_version, global_arguments, local_path, remote_path
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
borgmatic_runtime_directory,
|
||||
):
|
||||
'''
|
||||
Given a repository configuration dict, a configuration dict, the local Borg version, global
|
||||
@@ -365,10 +373,12 @@ def collect_spot_check_source_paths(
|
||||
dry_run=True,
|
||||
repository_path=repository['path'],
|
||||
config=config,
|
||||
config_paths=(),
|
||||
source_directories=borgmatic.actions.create.process_source_directories(
|
||||
config,
|
||||
),
|
||||
local_borg_version=local_borg_version,
|
||||
global_arguments=global_arguments,
|
||||
borgmatic_runtime_directories=(),
|
||||
borgmatic_runtime_directory=borgmatic_runtime_directory,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
list_files=True,
|
||||
@@ -402,16 +412,22 @@ BORG_DIRECTORY_FILE_TYPE = 'd'
|
||||
|
||||
|
||||
def collect_spot_check_archive_paths(
|
||||
repository, archive, config, local_borg_version, global_arguments, local_path, remote_path
|
||||
repository,
|
||||
archive,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
borgmatic_runtime_directory,
|
||||
):
|
||||
'''
|
||||
Given a repository configuration dict, the name of the latest archive, a configuration dict, the
|
||||
local Borg version, global arguments as an argparse.Namespace instance, the local Borg path, and
|
||||
the remote Borg path, collect the paths from the given archive (but only include files and
|
||||
symlinks and exclude borgmatic runtime directories).
|
||||
local Borg version, global arguments as an argparse.Namespace instance, the local Borg path, the
|
||||
remote Borg path, and the borgmatic runtime directory, collect the paths from the given archive
|
||||
(but only include files and symlinks and exclude borgmatic runtime directories).
|
||||
'''
|
||||
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
||||
borgmatic_runtime_directory = borgmatic.config.paths.get_borgmatic_runtime_directory(config)
|
||||
|
||||
return tuple(
|
||||
path
|
||||
@@ -444,7 +460,7 @@ def compare_spot_check_hashes(
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
log_label,
|
||||
log_prefix,
|
||||
source_paths,
|
||||
):
|
||||
'''
|
||||
@@ -468,7 +484,7 @@ def compare_spot_check_hashes(
|
||||
if os.path.exists(os.path.join(working_directory or '', source_path))
|
||||
}
|
||||
logger.debug(
|
||||
f'{log_label}: Sampling {sample_count} source paths (~{spot_check_config["data_sample_percentage"]}%) for spot check'
|
||||
f'{log_prefix}: Sampling {sample_count} source paths (~{spot_check_config["data_sample_percentage"]}%) for spot check'
|
||||
)
|
||||
|
||||
source_sample_paths_iterator = iter(source_sample_paths)
|
||||
@@ -545,18 +561,19 @@ def spot_check(
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
borgmatic_runtime_directory,
|
||||
):
|
||||
'''
|
||||
Given a repository dict, a loaded configuration dict, the local Borg version, global arguments
|
||||
as an argparse.Namespace instance, the local Borg path, and the remote Borg path, perform a spot
|
||||
check for the latest archive in the given repository.
|
||||
as an argparse.Namespace instance, the local Borg path, the remote Borg path, and the borgmatic
|
||||
runtime directory, perform a spot check for the latest archive in the given repository.
|
||||
|
||||
A spot check compares file counts and also the hashes for a random sampling of source files on
|
||||
disk to those stored in the latest archive. If any differences are beyond configured tolerances,
|
||||
then the check fails.
|
||||
'''
|
||||
log_label = f'{repository.get("label", repository["path"])}'
|
||||
logger.debug(f'{log_label}: Running spot check')
|
||||
log_prefix = f'{repository.get("label", repository["path"])}'
|
||||
logger.debug(f'{log_prefix}: Running spot check')
|
||||
|
||||
try:
|
||||
spot_check_config = next(
|
||||
@@ -577,8 +594,9 @@ def spot_check(
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
borgmatic_runtime_directory,
|
||||
)
|
||||
logger.debug(f'{log_label}: {len(source_paths)} total source paths for spot check')
|
||||
logger.debug(f'{log_prefix}: {len(source_paths)} total source paths for spot check')
|
||||
|
||||
archive = borgmatic.borg.repo_list.resolve_archive_name(
|
||||
repository['path'],
|
||||
@@ -589,7 +607,7 @@ def spot_check(
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
logger.debug(f'{log_label}: Using archive {archive} for spot check')
|
||||
logger.debug(f'{log_prefix}: Using archive {archive} for spot check')
|
||||
|
||||
archive_paths = collect_spot_check_archive_paths(
|
||||
repository,
|
||||
@@ -599,8 +617,9 @@ def spot_check(
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
borgmatic_runtime_directory,
|
||||
)
|
||||
logger.debug(f'{log_label}: {len(archive_paths)} total archive paths for spot check')
|
||||
logger.debug(f'{log_prefix}: {len(archive_paths)} total archive paths for spot check')
|
||||
|
||||
# Calculate the percentage delta between the source paths count and the archive paths count, and
|
||||
# compare that delta to the configured count tolerance percentage.
|
||||
@@ -608,10 +627,10 @@ def spot_check(
|
||||
|
||||
if count_delta_percentage > spot_check_config['count_tolerance_percentage']:
|
||||
logger.debug(
|
||||
f'{log_label}: Paths in source paths but not latest archive: {", ".join(set(source_paths) - set(archive_paths)) or "none"}'
|
||||
f'{log_prefix}: Paths in source paths but not latest archive: {", ".join(set(source_paths) - set(archive_paths)) or "none"}'
|
||||
)
|
||||
logger.debug(
|
||||
f'{log_label}: Paths in latest archive but not source paths: {", ".join(set(archive_paths) - set(source_paths)) or "none"}'
|
||||
f'{log_prefix}: Paths in latest archive but not source paths: {", ".join(set(archive_paths) - set(source_paths)) or "none"}'
|
||||
)
|
||||
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"]}%)'
|
||||
@@ -625,25 +644,25 @@ def spot_check(
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
log_label,
|
||||
log_prefix,
|
||||
source_paths,
|
||||
)
|
||||
|
||||
# Error if the percentage of failing hashes exceeds the configured tolerance percentage.
|
||||
logger.debug(f'{log_label}: {len(failing_paths)} non-matching spot check hashes')
|
||||
logger.debug(f'{log_prefix}: {len(failing_paths)} non-matching spot check hashes')
|
||||
data_tolerance_percentage = spot_check_config['data_tolerance_percentage']
|
||||
failing_percentage = (len(failing_paths) / len(source_paths)) * 100
|
||||
|
||||
if failing_percentage > data_tolerance_percentage:
|
||||
logger.debug(
|
||||
f'{log_label}: Source paths with data not matching the latest archive: {", ".join(failing_paths)}'
|
||||
f'{log_prefix}: Source paths with data not matching the latest archive: {", ".join(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}%)'
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f'{log_label}: Spot check passed with a {count_delta_percentage:.2f}% file count delta and a {failing_percentage:.2f}% file data delta'
|
||||
f'{log_prefix}: Spot check passed with a {count_delta_percentage:.2f}% file count delta and a {failing_percentage:.2f}% file data delta'
|
||||
)
|
||||
|
||||
|
||||
@@ -677,7 +696,9 @@ def run_check(
|
||||
**hook_context,
|
||||
)
|
||||
|
||||
logger.info(f'{repository.get("label", repository["path"])}: Running consistency checks')
|
||||
log_prefix = repository.get('label', repository['path'])
|
||||
logger.info(f'{log_prefix}: Running consistency checks')
|
||||
|
||||
repository_id = borgmatic.borg.check.get_repository_id(
|
||||
repository['path'],
|
||||
config,
|
||||
@@ -729,14 +750,18 @@ def run_check(
|
||||
write_check_time(make_check_time_path(config, repository_id, 'extract'))
|
||||
|
||||
if 'spot' in checks:
|
||||
spot_check(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
with borgmatic.config.paths.Runtime_directory(
|
||||
config, log_prefix
|
||||
) as borgmatic_runtime_directory:
|
||||
spot_check(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
borgmatic_runtime_directory,
|
||||
)
|
||||
write_check_time(make_check_time_path(config, repository_id, 'spot'))
|
||||
|
||||
borgmatic.hooks.command.execute_hook(
|
||||
|
||||
@@ -38,37 +38,44 @@ def get_config_paths(archive_name, bootstrap_arguments, global_arguments, local_
|
||||
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(
|
||||
{'borgmatic_source_directory': bootstrap_arguments.borgmatic_source_directory}
|
||||
)
|
||||
borgmatic_runtime_directory = borgmatic.config.paths.get_borgmatic_runtime_directory(
|
||||
{'user_runtime_directory': bootstrap_arguments.user_runtime_directory}
|
||||
)
|
||||
config = make_bootstrap_config(bootstrap_arguments)
|
||||
|
||||
# Probe for the manifest file in multiple locations, as the default location has moved to the
|
||||
# borgmatic runtime directory (which get stored as just "/borgmatic" with Borg 1.4+). But we
|
||||
# still want to support reading the manifest from previously created archives as well.
|
||||
for base_directory in ('borgmatic', borgmatic_runtime_directory, borgmatic_source_directory):
|
||||
borgmatic_manifest_path = os.path.join(base_directory, 'bootstrap', 'manifest.json')
|
||||
with borgmatic.config.paths.Runtime_directory(
|
||||
{'user_runtime_directory': bootstrap_arguments.user_runtime_directory},
|
||||
bootstrap_arguments.repository,
|
||||
) as borgmatic_runtime_directory:
|
||||
for base_directory in (
|
||||
'borgmatic',
|
||||
borgmatic.config.paths.make_runtime_directory_glob(borgmatic_runtime_directory),
|
||||
borgmatic_source_directory,
|
||||
):
|
||||
borgmatic_manifest_path = 'sh:' + os.path.join(
|
||||
base_directory, 'bootstrap', 'manifest.json'
|
||||
)
|
||||
|
||||
extract_process = borgmatic.borg.extract.extract_archive(
|
||||
global_arguments.dry_run,
|
||||
bootstrap_arguments.repository,
|
||||
archive_name,
|
||||
[borgmatic_manifest_path],
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path=bootstrap_arguments.local_path,
|
||||
remote_path=bootstrap_arguments.remote_path,
|
||||
extract_to_stdout=True,
|
||||
)
|
||||
manifest_json = extract_process.stdout.read()
|
||||
extract_process = borgmatic.borg.extract.extract_archive(
|
||||
global_arguments.dry_run,
|
||||
bootstrap_arguments.repository,
|
||||
archive_name,
|
||||
[borgmatic_manifest_path],
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path=bootstrap_arguments.local_path,
|
||||
remote_path=bootstrap_arguments.remote_path,
|
||||
extract_to_stdout=True,
|
||||
)
|
||||
manifest_json = extract_process.stdout.read()
|
||||
|
||||
if manifest_json:
|
||||
break
|
||||
else:
|
||||
raise ValueError(
|
||||
'Cannot read configuration paths from archive due to missing bootstrap manifest'
|
||||
)
|
||||
if manifest_json:
|
||||
break
|
||||
else:
|
||||
raise ValueError(
|
||||
'Cannot read configuration paths from archive due to missing bootstrap manifest'
|
||||
)
|
||||
|
||||
try:
|
||||
manifest_data = json.loads(manifest_json)
|
||||
|
||||
+180
-62
@@ -1,7 +1,8 @@
|
||||
import importlib.metadata
|
||||
import json
|
||||
import glob
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
import borgmatic.actions.json
|
||||
import borgmatic.borg.create
|
||||
@@ -14,30 +15,133 @@ import borgmatic.hooks.dump
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_borgmatic_manifest(config, config_paths, dry_run):
|
||||
def expand_directory(directory, working_directory):
|
||||
'''
|
||||
Create a borgmatic manifest file to store the paths to the configuration files used to create
|
||||
the archive.
|
||||
Given a directory path, expand any tilde (representing a user's home directory) and any globs
|
||||
therein. Return a list of one or more resulting paths.
|
||||
'''
|
||||
if dry_run:
|
||||
return
|
||||
expanded_directory = os.path.join(working_directory or '', os.path.expanduser(directory))
|
||||
|
||||
borgmatic_runtime_directory = borgmatic.config.paths.get_borgmatic_runtime_directory(config)
|
||||
borgmatic_manifest_path = os.path.join(
|
||||
borgmatic_runtime_directory, 'bootstrap', 'manifest.json'
|
||||
return glob.glob(expanded_directory) or [expanded_directory]
|
||||
|
||||
|
||||
def expand_directories(directories, working_directory=None):
|
||||
'''
|
||||
Given a sequence of directory paths and an optional working directory, expand tildes and globs
|
||||
in each one. Return all the resulting directories as a single flattened tuple.
|
||||
'''
|
||||
if directories is None:
|
||||
return ()
|
||||
|
||||
return tuple(
|
||||
itertools.chain.from_iterable(
|
||||
expand_directory(directory, working_directory) for directory in directories
|
||||
)
|
||||
)
|
||||
|
||||
if not os.path.exists(borgmatic_manifest_path):
|
||||
os.makedirs(os.path.dirname(borgmatic_manifest_path), exist_ok=True)
|
||||
|
||||
with open(borgmatic_manifest_path, 'w') as config_list_file:
|
||||
json.dump(
|
||||
{
|
||||
'borgmatic_version': importlib.metadata.version('borgmatic'),
|
||||
'config_paths': config_paths,
|
||||
},
|
||||
config_list_file,
|
||||
)
|
||||
def map_directories_to_devices(directories, working_directory=None):
|
||||
'''
|
||||
Given a sequence of directories and an optional working directory, return a map from directory
|
||||
to an identifier for the device on which that directory resides or None if the path doesn't
|
||||
exist.
|
||||
|
||||
This is handy for determining whether two different directories are on the same filesystem (have
|
||||
the same device identifier).
|
||||
'''
|
||||
return {
|
||||
directory: os.stat(full_directory).st_dev if os.path.exists(full_directory) else None
|
||||
for directory in directories
|
||||
for full_directory in (os.path.join(working_directory or '', directory),)
|
||||
}
|
||||
|
||||
|
||||
def deduplicate_directories(directory_devices, additional_directory_devices):
|
||||
'''
|
||||
Given a map from directory to the identifier for the device on which that directory resides,
|
||||
return the directories as a sorted sequence with all duplicate child directories removed. For
|
||||
instance, if paths is ['/foo', '/foo/bar'], return just: ['/foo']
|
||||
|
||||
The one exception to this rule is if two paths are on different filesystems (devices). In that
|
||||
case, they won't get de-duplicated in case they both need to be passed to Borg (e.g. the
|
||||
location.one_file_system option is true).
|
||||
|
||||
The idea is that if Borg is given a parent directory, then it doesn't also need to be given
|
||||
child directories, because it will naturally spider the contents of the parent directory. And
|
||||
there are cases where Borg coming across the same file twice will result in duplicate reads and
|
||||
even hangs, e.g. when a database hook is using a named pipe for streaming database dumps to
|
||||
Borg.
|
||||
|
||||
If any additional directory devices are given, also deduplicate against them, but don't include
|
||||
them in the returned directories.
|
||||
'''
|
||||
deduplicated = set()
|
||||
directories = sorted(directory_devices.keys())
|
||||
additional_directories = sorted(additional_directory_devices.keys())
|
||||
all_devices = {**directory_devices, **additional_directory_devices}
|
||||
|
||||
for directory in directories:
|
||||
deduplicated.add(directory)
|
||||
parents = pathlib.PurePath(directory).parents
|
||||
|
||||
# If another directory in the given list (or the additional list) is a parent of current
|
||||
# directory (even n levels up) and both are on the same filesystem, then the current
|
||||
# directory is a duplicate.
|
||||
for other_directory in directories + additional_directories:
|
||||
for parent in parents:
|
||||
if (
|
||||
pathlib.PurePath(other_directory) == parent
|
||||
and all_devices[directory] is not None
|
||||
and all_devices[other_directory] == all_devices[directory]
|
||||
):
|
||||
if directory in deduplicated:
|
||||
deduplicated.remove(directory)
|
||||
break
|
||||
|
||||
return sorted(deduplicated)
|
||||
|
||||
|
||||
ROOT_PATTERN_PREFIX = 'R '
|
||||
|
||||
|
||||
def pattern_root_directories(patterns=None):
|
||||
'''
|
||||
Given a sequence of patterns, parse out and return just the root directories.
|
||||
'''
|
||||
if not patterns:
|
||||
return []
|
||||
|
||||
return [
|
||||
pattern.split(ROOT_PATTERN_PREFIX, maxsplit=1)[1]
|
||||
for pattern in patterns
|
||||
if pattern.startswith(ROOT_PATTERN_PREFIX)
|
||||
]
|
||||
|
||||
|
||||
def process_source_directories(config, source_directories=None):
|
||||
'''
|
||||
Given a sequence of source directories (either in the source_directories argument or, lacking
|
||||
that, from config), expand and deduplicate the source directories, returning the result.
|
||||
'''
|
||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
||||
|
||||
if source_directories is None:
|
||||
source_directories = tuple(config.get('source_directories', ()))
|
||||
|
||||
return deduplicate_directories(
|
||||
map_directories_to_devices(
|
||||
expand_directories(
|
||||
tuple(source_directories),
|
||||
working_directory=working_directory,
|
||||
)
|
||||
),
|
||||
additional_directory_devices=map_directories_to_devices(
|
||||
expand_directories(
|
||||
pattern_root_directories(config.get('patterns')),
|
||||
working_directory=working_directory,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def run_create(
|
||||
@@ -71,54 +175,68 @@ def run_create(
|
||||
global_arguments.dry_run,
|
||||
**hook_context,
|
||||
)
|
||||
logger.info(f'{repository.get("label", repository["path"])}: Creating archive{dry_run_label}')
|
||||
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||
'remove_data_source_dumps',
|
||||
config,
|
||||
repository['path'],
|
||||
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
active_dumps = borgmatic.hooks.dispatch.call_hooks(
|
||||
'dump_data_sources',
|
||||
config,
|
||||
repository['path'],
|
||||
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
if config.get('store_config_files', True):
|
||||
create_borgmatic_manifest(
|
||||
|
||||
log_prefix = repository.get('label', repository['path'])
|
||||
logger.info(f'{log_prefix}: Creating archive{dry_run_label}')
|
||||
|
||||
with borgmatic.config.paths.Runtime_directory(
|
||||
config, log_prefix
|
||||
) as borgmatic_runtime_directory:
|
||||
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||
'remove_data_source_dumps',
|
||||
config,
|
||||
repository['path'],
|
||||
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
|
||||
borgmatic_runtime_directory,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
source_directories = process_source_directories(config)
|
||||
active_dumps = borgmatic.hooks.dispatch.call_hooks(
|
||||
'dump_data_sources',
|
||||
config,
|
||||
repository['path'],
|
||||
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
|
||||
config_paths,
|
||||
borgmatic_runtime_directory,
|
||||
source_directories,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
stream_processes = [process for processes in active_dumps.values() for process in processes]
|
||||
|
||||
json_output = borgmatic.borg.create.create_archive(
|
||||
global_arguments.dry_run,
|
||||
repository['path'],
|
||||
config,
|
||||
config_paths,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
progress=create_arguments.progress,
|
||||
stats=create_arguments.stats,
|
||||
json=create_arguments.json,
|
||||
list_files=create_arguments.list_files,
|
||||
stream_processes=stream_processes,
|
||||
)
|
||||
if json_output:
|
||||
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
|
||||
# Process source directories again in case any data source hooks updated them. Without this
|
||||
# step, we could end up with duplicate paths that cause Borg to hang when it tries to read
|
||||
# from the same named pipe twice.
|
||||
source_directories = process_source_directories(config, source_directories)
|
||||
stream_processes = [process for processes in active_dumps.values() for process in processes]
|
||||
|
||||
json_output = borgmatic.borg.create.create_archive(
|
||||
global_arguments.dry_run,
|
||||
repository['path'],
|
||||
config,
|
||||
source_directories,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
borgmatic_runtime_directory,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
progress=create_arguments.progress,
|
||||
stats=create_arguments.stats,
|
||||
json=create_arguments.json,
|
||||
list_files=create_arguments.list_files,
|
||||
stream_processes=stream_processes,
|
||||
)
|
||||
|
||||
if json_output:
|
||||
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
|
||||
|
||||
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||
'remove_data_source_dumps',
|
||||
config,
|
||||
config_filename,
|
||||
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
|
||||
borgmatic_runtime_directory,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
|
||||
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||
'remove_data_source_dumps',
|
||||
config,
|
||||
config_filename,
|
||||
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
borgmatic.hooks.command.execute_hook(
|
||||
config.get('after_backup'),
|
||||
config.get('umask'),
|
||||
|
||||
+113
-102
@@ -91,7 +91,9 @@ def strip_path_prefix_from_extracted_dump_destination(
|
||||
if not databases_directory.endswith('_databases'):
|
||||
continue
|
||||
|
||||
os.rename(subdirectory_path, os.path.join(borgmatic_runtime_directory, databases_directory))
|
||||
shutil.move(
|
||||
subdirectory_path, os.path.join(borgmatic_runtime_directory, databases_directory)
|
||||
)
|
||||
break
|
||||
|
||||
|
||||
@@ -106,6 +108,7 @@ def restore_single_data_source(
|
||||
hook_name,
|
||||
data_source,
|
||||
connection_params,
|
||||
borgmatic_runtime_directory,
|
||||
):
|
||||
'''
|
||||
Given (among other things) an archive name, a data source hook name, the hostname, port,
|
||||
@@ -121,9 +124,9 @@ def restore_single_data_source(
|
||||
config,
|
||||
repository['path'],
|
||||
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
|
||||
borgmatic_runtime_directory,
|
||||
data_source['name'],
|
||||
)[hook_name]
|
||||
borgmatic_runtime_directory = borgmatic.config.paths.get_borgmatic_runtime_directory(config)
|
||||
|
||||
destination_path = (
|
||||
tempfile.mkdtemp(dir=borgmatic_runtime_directory)
|
||||
@@ -133,7 +136,7 @@ def restore_single_data_source(
|
||||
|
||||
try:
|
||||
# Kick off a single data source extract. If using a directory format, extract to a temporary
|
||||
# directory. Otheriwes extract the single dump file to stdout.
|
||||
# directory. Otherwise extract the single dump file to stdout.
|
||||
extract_process = borgmatic.borg.extract.extract_archive(
|
||||
dry_run=global_arguments.dry_run,
|
||||
repository=repository['path'],
|
||||
@@ -168,6 +171,7 @@ def restore_single_data_source(
|
||||
dry_run=global_arguments.dry_run,
|
||||
extract_process=extract_process,
|
||||
connection_params=connection_params,
|
||||
borgmatic_runtime_directory=borgmatic_runtime_directory,
|
||||
)
|
||||
|
||||
|
||||
@@ -179,17 +183,17 @@ def collect_archive_data_source_names(
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
borgmatic_runtime_directory,
|
||||
):
|
||||
'''
|
||||
Given a local or remote repository path, a resolved archive name, a configuration dict, the
|
||||
local Borg version, global_arguments an argparse.Namespace, and local and remote Borg paths,
|
||||
query the archive for the names of data sources it contains as dumps and return them as a dict
|
||||
from hook name to a sequence of data source names.
|
||||
local Borg version, global_arguments an argparse.Namespace, local and remote Borg paths, and the
|
||||
borgmatic runtime directory, query the archive for the names of data sources it contains as
|
||||
dumps and return them as a dict from hook name to a sequence of data source names.
|
||||
'''
|
||||
borgmatic_source_directory = str(
|
||||
pathlib.Path(borgmatic.config.paths.get_borgmatic_source_directory(config))
|
||||
)
|
||||
borgmatic_runtime_directory = borgmatic.config.paths.get_borgmatic_runtime_directory(config)
|
||||
|
||||
# Probe for the data source dumps in multiple locations, as the default location has moved to
|
||||
# the borgmatic runtime directory (which get stored as just "/borgmatic" with Borg 1.4+). But we
|
||||
@@ -205,7 +209,7 @@ def collect_archive_data_source_names(
|
||||
+ borgmatic.hooks.dump.make_data_source_dump_path(base_directory, '*_databases/*/*')
|
||||
for base_directory in (
|
||||
'borgmatic',
|
||||
borgmatic_runtime_directory.lstrip('/'),
|
||||
borgmatic.config.paths.make_runtime_directory_glob(borgmatic_runtime_directory),
|
||||
borgmatic_source_directory.lstrip('/'),
|
||||
)
|
||||
],
|
||||
@@ -340,109 +344,116 @@ def run_restore(
|
||||
):
|
||||
return
|
||||
|
||||
logger.info(
|
||||
f'{repository.get("label", repository["path"])}: Restoring data sources from archive {restore_arguments.archive}'
|
||||
)
|
||||
log_prefix = repository.get('label', repository['path'])
|
||||
logger.info(f'{log_prefix}: Restoring data sources from archive {restore_arguments.archive}')
|
||||
|
||||
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||
'remove_data_source_dumps',
|
||||
config,
|
||||
repository['path'],
|
||||
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
with borgmatic.config.paths.Runtime_directory(
|
||||
config, log_prefix
|
||||
) as borgmatic_runtime_directory:
|
||||
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||
'remove_data_source_dumps',
|
||||
config,
|
||||
repository['path'],
|
||||
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
|
||||
borgmatic_runtime_directory,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
|
||||
archive_name = borgmatic.borg.repo_list.resolve_archive_name(
|
||||
repository['path'],
|
||||
restore_arguments.archive,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
archive_data_source_names = collect_archive_data_source_names(
|
||||
repository['path'],
|
||||
archive_name,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
restore_names = find_data_sources_to_restore(
|
||||
restore_arguments.data_sources, archive_data_source_names
|
||||
)
|
||||
found_names = set()
|
||||
remaining_restore_names = {}
|
||||
connection_params = {
|
||||
'hostname': restore_arguments.hostname,
|
||||
'port': restore_arguments.port,
|
||||
'username': restore_arguments.username,
|
||||
'password': restore_arguments.password,
|
||||
'restore_path': restore_arguments.restore_path,
|
||||
}
|
||||
archive_name = borgmatic.borg.repo_list.resolve_archive_name(
|
||||
repository['path'],
|
||||
restore_arguments.archive,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
archive_data_source_names = collect_archive_data_source_names(
|
||||
repository['path'],
|
||||
archive_name,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
borgmatic_runtime_directory,
|
||||
)
|
||||
restore_names = find_data_sources_to_restore(
|
||||
restore_arguments.data_sources, archive_data_source_names
|
||||
)
|
||||
found_names = set()
|
||||
remaining_restore_names = {}
|
||||
connection_params = {
|
||||
'hostname': restore_arguments.hostname,
|
||||
'port': restore_arguments.port,
|
||||
'username': restore_arguments.username,
|
||||
'password': restore_arguments.password,
|
||||
'restore_path': restore_arguments.restore_path,
|
||||
}
|
||||
|
||||
for hook_name, data_source_names in restore_names.items():
|
||||
for data_source_name in data_source_names:
|
||||
found_hook_name, found_data_source = get_configured_data_source(
|
||||
config, archive_data_source_names, hook_name, data_source_name
|
||||
)
|
||||
|
||||
if not found_data_source:
|
||||
remaining_restore_names.setdefault(found_hook_name or hook_name, []).append(
|
||||
data_source_name
|
||||
for hook_name, data_source_names in restore_names.items():
|
||||
for data_source_name in data_source_names:
|
||||
found_hook_name, found_data_source = get_configured_data_source(
|
||||
config, archive_data_source_names, hook_name, data_source_name
|
||||
)
|
||||
continue
|
||||
|
||||
found_names.add(data_source_name)
|
||||
restore_single_data_source(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
archive_name,
|
||||
found_hook_name or hook_name,
|
||||
dict(found_data_source, **{'schemas': restore_arguments.schemas}),
|
||||
connection_params,
|
||||
)
|
||||
if not found_data_source:
|
||||
remaining_restore_names.setdefault(found_hook_name or hook_name, []).append(
|
||||
data_source_name
|
||||
)
|
||||
continue
|
||||
|
||||
# For any data sources that weren't found via exact matches in the configuration, try to
|
||||
# fallback to "all" entries.
|
||||
for hook_name, data_source_names in remaining_restore_names.items():
|
||||
for data_source_name in data_source_names:
|
||||
found_hook_name, found_data_source = get_configured_data_source(
|
||||
config, archive_data_source_names, hook_name, data_source_name, 'all'
|
||||
)
|
||||
found_names.add(data_source_name)
|
||||
restore_single_data_source(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
archive_name,
|
||||
found_hook_name or hook_name,
|
||||
dict(found_data_source, **{'schemas': restore_arguments.schemas}),
|
||||
connection_params,
|
||||
borgmatic_runtime_directory,
|
||||
)
|
||||
|
||||
if not found_data_source:
|
||||
continue
|
||||
# For any data sources that weren't found via exact matches in the configuration, try to
|
||||
# fallback to "all" entries.
|
||||
for hook_name, data_source_names in remaining_restore_names.items():
|
||||
for data_source_name in data_source_names:
|
||||
found_hook_name, found_data_source = get_configured_data_source(
|
||||
config, archive_data_source_names, hook_name, data_source_name, 'all'
|
||||
)
|
||||
|
||||
found_names.add(data_source_name)
|
||||
data_source = copy.copy(found_data_source)
|
||||
data_source['name'] = data_source_name
|
||||
if not found_data_source:
|
||||
continue
|
||||
|
||||
restore_single_data_source(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
archive_name,
|
||||
found_hook_name or hook_name,
|
||||
dict(data_source, **{'schemas': restore_arguments.schemas}),
|
||||
connection_params,
|
||||
)
|
||||
found_names.add(data_source_name)
|
||||
data_source = copy.copy(found_data_source)
|
||||
data_source['name'] = data_source_name
|
||||
|
||||
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||
'remove_data_source_dumps',
|
||||
config,
|
||||
repository['path'],
|
||||
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
restore_single_data_source(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
archive_name,
|
||||
found_hook_name or hook_name,
|
||||
dict(data_source, **{'schemas': restore_arguments.schemas}),
|
||||
connection_params,
|
||||
borgmatic_runtime_directory,
|
||||
)
|
||||
|
||||
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||
'remove_data_source_dumps',
|
||||
config,
|
||||
repository['path'],
|
||||
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
|
||||
borgmatic_runtime_directory,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
|
||||
ensure_data_sources_found(restore_names, remaining_restore_names, found_names)
|
||||
|
||||
@@ -150,6 +150,7 @@ def check_archives(
|
||||
)
|
||||
|
||||
max_duration = check_arguments.max_duration or repository_check_config.get('max_duration')
|
||||
umask = config.get('umask')
|
||||
|
||||
borg_environment = environment.make_environment(config)
|
||||
borg_exit_codes = config.get('borg_exit_codes')
|
||||
@@ -160,6 +161,7 @@ def check_archives(
|
||||
+ (('--max-duration', str(max_duration)) if max_duration else ())
|
||||
+ make_check_name_flags(checks, archive_filter_flags)
|
||||
+ (('--remote-path', remote_path) if remote_path else ())
|
||||
+ (('--umask', str(umask)) if umask else ())
|
||||
+ (('--log-json',) if global_arguments.log_json else ())
|
||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||
+ verbosity_flags
|
||||
|
||||
+16
-158
@@ -1,4 +1,3 @@
|
||||
import glob
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
@@ -20,31 +19,6 @@ from borgmatic.execute import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def expand_directory(directory, working_directory):
|
||||
'''
|
||||
Given a directory path, expand any tilde (representing a user's home directory) and any globs
|
||||
therein. Return a list of one or more resulting paths.
|
||||
'''
|
||||
expanded_directory = os.path.join(working_directory or '', os.path.expanduser(directory))
|
||||
|
||||
return glob.glob(expanded_directory) or [expanded_directory]
|
||||
|
||||
|
||||
def expand_directories(directories, working_directory=None):
|
||||
'''
|
||||
Given a sequence of directory paths and an optional working directory, expand tildes and globs
|
||||
in each one. Return all the resulting directories as a single flattened tuple.
|
||||
'''
|
||||
if directories is None:
|
||||
return ()
|
||||
|
||||
return tuple(
|
||||
itertools.chain.from_iterable(
|
||||
expand_directory(directory, working_directory) for directory in directories
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def expand_home_directories(directories):
|
||||
'''
|
||||
Given a sequence of directory paths, expand tildes in each one. Do not perform any globbing.
|
||||
@@ -56,67 +30,6 @@ def expand_home_directories(directories):
|
||||
return tuple(os.path.expanduser(directory) for directory in directories)
|
||||
|
||||
|
||||
def map_directories_to_devices(directories, working_directory=None):
|
||||
'''
|
||||
Given a sequence of directories and an optional working directory, return a map from directory
|
||||
to an identifier for the device on which that directory resides or None if the path doesn't
|
||||
exist.
|
||||
|
||||
This is handy for determining whether two different directories are on the same filesystem (have
|
||||
the same device identifier).
|
||||
'''
|
||||
return {
|
||||
directory: os.stat(full_directory).st_dev if os.path.exists(full_directory) else None
|
||||
for directory in directories
|
||||
for full_directory in (os.path.join(working_directory or '', directory),)
|
||||
}
|
||||
|
||||
|
||||
def deduplicate_directories(directory_devices, additional_directory_devices):
|
||||
'''
|
||||
Given a map from directory to the identifier for the device on which that directory resides,
|
||||
return the directories as a sorted tuple with all duplicate child directories removed. For
|
||||
instance, if paths is ('/foo', '/foo/bar'), return just: ('/foo',)
|
||||
|
||||
The one exception to this rule is if two paths are on different filesystems (devices). In that
|
||||
case, they won't get de-duplicated in case they both need to be passed to Borg (e.g. the
|
||||
location.one_file_system option is true).
|
||||
|
||||
The idea is that if Borg is given a parent directory, then it doesn't also need to be given
|
||||
child directories, because it will naturally spider the contents of the parent directory. And
|
||||
there are cases where Borg coming across the same file twice will result in duplicate reads and
|
||||
even hangs, e.g. when a database hook is using a named pipe for streaming database dumps to
|
||||
Borg.
|
||||
|
||||
If any additional directory devices are given, also deduplicate against them, but don't include
|
||||
them in the returned directories.
|
||||
'''
|
||||
deduplicated = set()
|
||||
directories = sorted(directory_devices.keys())
|
||||
additional_directories = sorted(additional_directory_devices.keys())
|
||||
all_devices = {**directory_devices, **additional_directory_devices}
|
||||
|
||||
for directory in directories:
|
||||
deduplicated.add(directory)
|
||||
parents = pathlib.PurePath(directory).parents
|
||||
|
||||
# If another directory in the given list (or the additional list) is a parent of current
|
||||
# directory (even n levels up) and both are on the same filesystem, then the current
|
||||
# directory is a duplicate.
|
||||
for other_directory in directories + additional_directories:
|
||||
for parent in parents:
|
||||
if (
|
||||
pathlib.PurePath(other_directory) == parent
|
||||
and all_devices[directory] is not None
|
||||
and all_devices[other_directory] == all_devices[directory]
|
||||
):
|
||||
if directory in deduplicated:
|
||||
deduplicated.remove(directory)
|
||||
break
|
||||
|
||||
return tuple(sorted(deduplicated))
|
||||
|
||||
|
||||
def write_pattern_file(patterns=None, sources=None, pattern_file=None):
|
||||
'''
|
||||
Given a sequence of patterns and an optional sequence of source directories, write them to a
|
||||
@@ -221,32 +134,6 @@ def make_list_filter_flags(local_borg_version, dry_run):
|
||||
return f'{base_flags}-'
|
||||
|
||||
|
||||
def collect_borgmatic_runtime_directories(borgmatic_runtime_directory):
|
||||
'''
|
||||
Return a list of borgmatic-specific runtime directories used for temporary runtime data like
|
||||
streaming database dumps and bootstrap metadata. If no such directories exist, return an empty
|
||||
list.
|
||||
'''
|
||||
return [borgmatic_runtime_directory] if os.path.exists(borgmatic_runtime_directory) else []
|
||||
|
||||
|
||||
ROOT_PATTERN_PREFIX = 'R '
|
||||
|
||||
|
||||
def pattern_root_directories(patterns=None):
|
||||
'''
|
||||
Given a sequence of patterns, parse out and return just the root directories.
|
||||
'''
|
||||
if not patterns:
|
||||
return []
|
||||
|
||||
return [
|
||||
pattern.split(ROOT_PATTERN_PREFIX, maxsplit=1)[1]
|
||||
for pattern in patterns
|
||||
if pattern.startswith(ROOT_PATTERN_PREFIX)
|
||||
]
|
||||
|
||||
|
||||
def special_file(path):
|
||||
'''
|
||||
Return whether the given path is a special file (character device, block device, or named pipe
|
||||
@@ -307,21 +194,15 @@ def collect_special_file_paths(
|
||||
)
|
||||
|
||||
|
||||
def check_all_source_directories_exist(source_directories, working_directory=None):
|
||||
def check_all_source_directories_exist(source_directories):
|
||||
'''
|
||||
Given a sequence of source directories and an optional working directory to serve as a prefix
|
||||
for each (if it's a relative directory), check that the source directories all exist. If any do
|
||||
Given a sequence of source directories, check that the source directories all exist. If any do
|
||||
not, raise an exception.
|
||||
'''
|
||||
missing_directories = [
|
||||
source_directory
|
||||
for source_directory in source_directories
|
||||
if not all(
|
||||
[
|
||||
os.path.exists(os.path.join(working_directory or '', directory))
|
||||
for directory in expand_directory(source_directory, working_directory)
|
||||
]
|
||||
)
|
||||
if not os.path.exists(source_directory)
|
||||
]
|
||||
if missing_directories:
|
||||
raise ValueError(f"Source directories do not exist: {', '.join(missing_directories)}")
|
||||
@@ -334,10 +215,10 @@ def make_base_create_command(
|
||||
dry_run,
|
||||
repository_path,
|
||||
config,
|
||||
config_paths,
|
||||
source_directories,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
borgmatic_runtime_directories,
|
||||
borgmatic_runtime_directory,
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
progress=False,
|
||||
@@ -352,34 +233,13 @@ def make_base_create_command(
|
||||
(base Borg create command flags, Borg create command positional arguments, open pattern file
|
||||
handle, open exclude file handle).
|
||||
'''
|
||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
||||
|
||||
if config.get('source_directories_must_exist', False):
|
||||
check_all_source_directories_exist(
|
||||
config.get('source_directories'), working_directory=working_directory
|
||||
)
|
||||
|
||||
sources = deduplicate_directories(
|
||||
map_directories_to_devices(
|
||||
expand_directories(
|
||||
tuple(config.get('source_directories', ()))
|
||||
+ borgmatic_runtime_directories
|
||||
+ tuple(config_paths if config.get('store_config_files', True) else ()),
|
||||
working_directory=working_directory,
|
||||
)
|
||||
),
|
||||
additional_directory_devices=map_directories_to_devices(
|
||||
expand_directories(
|
||||
pattern_root_directories(config.get('patterns')),
|
||||
working_directory=working_directory,
|
||||
)
|
||||
),
|
||||
)
|
||||
check_all_source_directories_exist(source_directories)
|
||||
|
||||
ensure_files_readable(config.get('patterns_from'), config.get('exclude_from'))
|
||||
|
||||
pattern_file = (
|
||||
write_pattern_file(config.get('patterns'), sources)
|
||||
write_pattern_file(config.get('patterns'), source_directories)
|
||||
if config.get('patterns') or config.get('patterns_from')
|
||||
else None
|
||||
)
|
||||
@@ -457,7 +317,7 @@ def make_base_create_command(
|
||||
|
||||
create_positional_arguments = flags.make_repository_archive_flags(
|
||||
repository_path, archive_name_format, local_borg_version
|
||||
) + (sources if not pattern_file else ())
|
||||
) + (tuple(source_directories) if not pattern_file else ())
|
||||
|
||||
# If database hooks are enabled (as indicated by streaming processes), exclude files that might
|
||||
# cause Borg to hang. But skip this if the user has explicitly set the "read_special" to True.
|
||||
@@ -466,6 +326,7 @@ def make_base_create_command(
|
||||
f'{repository_path}: Ignoring configured "read_special" value of false, as true is needed for database hooks.'
|
||||
)
|
||||
borg_environment = environment.make_environment(config)
|
||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
||||
|
||||
logger.debug(f'{repository_path}: Collecting special file paths')
|
||||
special_file_paths = collect_special_file_paths(
|
||||
@@ -474,7 +335,9 @@ def make_base_create_command(
|
||||
local_path,
|
||||
working_directory,
|
||||
borg_environment,
|
||||
skip_directories=borgmatic_runtime_directories,
|
||||
skip_directories=(
|
||||
[borgmatic_runtime_directory] if os.path.exists(borgmatic_runtime_directory) else []
|
||||
),
|
||||
)
|
||||
|
||||
if special_file_paths:
|
||||
@@ -501,9 +364,10 @@ def create_archive(
|
||||
dry_run,
|
||||
repository_path,
|
||||
config,
|
||||
config_paths,
|
||||
source_directories,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
borgmatic_runtime_directory,
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
progress=False,
|
||||
@@ -523,22 +387,16 @@ def create_archive(
|
||||
borgmatic.logger.add_custom_log_levels()
|
||||
|
||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
||||
borgmatic_runtime_directories = expand_directories(
|
||||
collect_borgmatic_runtime_directories(
|
||||
borgmatic.config.paths.get_borgmatic_runtime_directory(config)
|
||||
),
|
||||
working_directory=working_directory,
|
||||
)
|
||||
|
||||
(create_flags, create_positional_arguments, pattern_file, exclude_file) = (
|
||||
make_base_create_command(
|
||||
dry_run,
|
||||
repository_path,
|
||||
config,
|
||||
config_paths,
|
||||
source_directories,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
borgmatic_runtime_directories,
|
||||
borgmatic_runtime_directory,
|
||||
local_path,
|
||||
remote_path,
|
||||
progress,
|
||||
|
||||
@@ -31,6 +31,7 @@ def make_delete_command(
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ borgmatic.borg.flags.make_flags('dry-run', global_arguments.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', global_arguments.log_json)
|
||||
+ borgmatic.borg.flags.make_flags('lock-wait', config.get('lock_wait'))
|
||||
+ borgmatic.borg.flags.make_flags('list', delete_arguments.list_archives)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import os
|
||||
|
||||
OPTION_TO_ENVIRONMENT_VARIABLE = {
|
||||
'borg_base_directory': 'BORG_BASE_DIR',
|
||||
'borg_config_directory': 'BORG_CONFIG_DIR',
|
||||
@@ -38,8 +40,9 @@ def make_environment(config):
|
||||
option_name,
|
||||
environment_variable_name,
|
||||
) in DEFAULT_BOOL_OPTION_TO_DOWNCASE_ENVIRONMENT_VARIABLE.items():
|
||||
value = config.get(option_name)
|
||||
environment[environment_variable_name] = 'yes' if value else 'no'
|
||||
if os.environ.get(environment_variable_name) is None:
|
||||
value = config.get(option_name)
|
||||
environment[environment_variable_name] = 'yes' if value else 'no'
|
||||
|
||||
for (
|
||||
option_name,
|
||||
|
||||
@@ -36,6 +36,7 @@ def make_info_command(
|
||||
else ()
|
||||
)
|
||||
+ flags.make_flags('remote-path', remote_path)
|
||||
+ flags.make_flags('umask', config.get('umask'))
|
||||
+ flags.make_flags('log-json', global_arguments.log_json)
|
||||
+ flags.make_flags('lock-wait', config.get('lock_wait'))
|
||||
+ (
|
||||
|
||||
@@ -34,8 +34,6 @@ def make_list_command(
|
||||
and local and remote Borg paths, return a command as a tuple to list archives or paths within an
|
||||
archive.
|
||||
'''
|
||||
lock_wait = config.get('lock_wait', None)
|
||||
|
||||
return (
|
||||
(local_path, 'list')
|
||||
+ (
|
||||
@@ -49,8 +47,9 @@ def make_list_command(
|
||||
else ()
|
||||
)
|
||||
+ flags.make_flags('remote-path', remote_path)
|
||||
+ flags.make_flags('umask', config.get('umask'))
|
||||
+ flags.make_flags('log-json', global_arguments.log_json)
|
||||
+ flags.make_flags('lock-wait', lock_wait)
|
||||
+ flags.make_flags('lock-wait', config.get('lock_wait'))
|
||||
+ flags.make_flags_from_arguments(list_arguments, excludes=MAKE_FLAGS_EXCLUDES)
|
||||
+ (
|
||||
flags.make_repository_archive_flags(
|
||||
|
||||
@@ -64,6 +64,7 @@ def create_repository(
|
||||
raise
|
||||
|
||||
lock_wait = config.get('lock_wait')
|
||||
umask = config.get('umask')
|
||||
extra_borg_options = config.get('extra_borg_options', {}).get('repo-create', '')
|
||||
|
||||
repo_create_command = (
|
||||
@@ -84,6 +85,7 @@ def create_repository(
|
||||
+ (('--log-json',) if global_arguments.log_json else ())
|
||||
+ (('--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 ())
|
||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||
)
|
||||
|
||||
@@ -36,6 +36,7 @@ def make_repo_delete_command(
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ borgmatic.borg.flags.make_flags('dry-run', global_arguments.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', global_arguments.log_json)
|
||||
+ borgmatic.borg.flags.make_flags('lock-wait', config.get('lock_wait'))
|
||||
+ borgmatic.borg.flags.make_flags('list', repo_delete_arguments.list_archives)
|
||||
|
||||
@@ -43,6 +43,7 @@ def display_repository_info(
|
||||
else ()
|
||||
)
|
||||
+ flags.make_flags('remote-path', remote_path)
|
||||
+ flags.make_flags('umask', config.get('umask'))
|
||||
+ flags.make_flags('log-json', global_arguments.log_json)
|
||||
+ flags.make_flags('lock-wait', lock_wait)
|
||||
+ (('--json',) if repo_info_arguments.json else ())
|
||||
|
||||
@@ -39,6 +39,7 @@ def resolve_archive_name(
|
||||
),
|
||||
)
|
||||
+ flags.make_flags('remote-path', remote_path)
|
||||
+ flags.make_flags('umask', config.get('umask'))
|
||||
+ flags.make_flags('log-json', global_arguments.log_json)
|
||||
+ flags.make_flags('lock-wait', config.get('lock_wait'))
|
||||
+ flags.make_flags('last', 1)
|
||||
@@ -100,6 +101,7 @@ def make_repo_list_command(
|
||||
else ()
|
||||
)
|
||||
+ flags.make_flags('remote-path', remote_path)
|
||||
+ flags.make_flags('umask', config.get('umask'))
|
||||
+ flags.make_flags('log-json', global_arguments.log_json)
|
||||
+ flags.make_flags('lock-wait', config.get('lock_wait'))
|
||||
+ (
|
||||
|
||||
@@ -30,6 +30,7 @@ def transfer_archives(
|
||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ flags.make_flags('remote-path', remote_path)
|
||||
+ flags.make_flags('umask', config.get('umask'))
|
||||
+ flags.make_flags('log-json', global_arguments.log_json)
|
||||
+ flags.make_flags('lock-wait', config.get('lock_wait', None))
|
||||
+ (
|
||||
|
||||
@@ -876,7 +876,7 @@ def make_parsers():
|
||||
)
|
||||
config_bootstrap_group.add_argument(
|
||||
'--user-runtime-directory',
|
||||
help='Path used for temporary runtime data like bootstrap metadata. Defaults to $XDG_RUNTIME_DIR or /var/run/$UID',
|
||||
help='Path used for temporary runtime data like bootstrap metadata. Defaults to $XDG_RUNTIME_DIR or $TMPDIR or $TEMP or /var/run/$UID',
|
||||
)
|
||||
config_bootstrap_group.add_argument(
|
||||
'--borgmatic-source-directory',
|
||||
@@ -1244,6 +1244,12 @@ def make_parsers():
|
||||
metavar='TIMESPAN',
|
||||
help='List archives that are newer than the specified time range (e.g. 7d or 12m) from the current time [Borg 2.x+ only]',
|
||||
)
|
||||
repo_list_group.add_argument(
|
||||
'--deleted',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="List only deleted archives that haven't yet been compacted [Borg 2.x+ only]",
|
||||
)
|
||||
repo_list_group.add_argument(
|
||||
'-h', '--help', action='help', help='Show this help message and exit'
|
||||
)
|
||||
|
||||
@@ -793,9 +793,6 @@ def collect_configuration_run_summary_logs(configs, config_paths, arguments):
|
||||
break
|
||||
|
||||
try:
|
||||
if 'extract' in arguments or 'mount' in arguments:
|
||||
validate.guard_single_repository_selected(repository, configs)
|
||||
|
||||
validate.guard_configuration_contains_repository(repository, configs)
|
||||
except ValueError as error:
|
||||
yield from log_error_records(str(error))
|
||||
|
||||
@@ -44,12 +44,12 @@ def schema_to_sample_configuration(schema, level=0, parent_is_sequence=False):
|
||||
if example is not None:
|
||||
return example
|
||||
|
||||
if schema_type == 'array':
|
||||
if schema_type == 'array' or (isinstance(schema_type, list) and 'array' in schema_type):
|
||||
config = ruamel.yaml.comments.CommentedSeq(
|
||||
[schema_to_sample_configuration(schema['items'], level, parent_is_sequence=True)]
|
||||
)
|
||||
add_comments_to_configuration_sequence(config, schema, indent=(level * INDENT))
|
||||
elif schema_type == 'object':
|
||||
elif schema_type == 'object' or (isinstance(schema_type, list) and 'object' in schema_type):
|
||||
config = ruamel.yaml.comments.CommentedMap(
|
||||
[
|
||||
(field_name, schema_to_sample_configuration(sub_schema, level + 1))
|
||||
|
||||
@@ -93,6 +93,25 @@ def normalize(config_filename, config):
|
||||
)
|
||||
config['exclude_if_present'] = [exclude_if_present]
|
||||
|
||||
# Unconditionally set the bootstrap hook so that it's enabled by default and config files get
|
||||
# stored in each Borg archive.
|
||||
config.setdefault('bootstrap', {})
|
||||
|
||||
# Move store_config_files from the global scope to the bootstrap hook.
|
||||
store_config_files = config.get('store_config_files')
|
||||
if store_config_files is not None:
|
||||
logs.append(
|
||||
logging.makeLogRecord(
|
||||
dict(
|
||||
levelno=logging.WARNING,
|
||||
levelname='WARNING',
|
||||
msg=f'{config_filename}: The store_config_files option has moved under the bootstrap hook. Specifying store_config_files at the global scope is deprecated and support will be removed from a future release.',
|
||||
)
|
||||
)
|
||||
)
|
||||
del config['store_config_files']
|
||||
config['bootstrap']['store_config_files'] = store_config_files
|
||||
|
||||
# Upgrade various monitoring hooks from a string to a dict.
|
||||
healthchecks = config.get('healthchecks')
|
||||
if isinstance(healthchecks, str):
|
||||
|
||||
+130
-20
@@ -1,4 +1,8 @@
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def expand_user_in_path(path):
|
||||
@@ -26,25 +30,132 @@ def get_borgmatic_source_directory(config):
|
||||
return expand_user_in_path(config.get('borgmatic_source_directory') or '~/.borgmatic')
|
||||
|
||||
|
||||
def get_borgmatic_runtime_directory(config):
|
||||
'''
|
||||
Given a configuration dict, get the borgmatic runtime directory used for storing temporary
|
||||
runtime data like streaming database dumps and bootstrap metadata. Defaults to
|
||||
$XDG_RUNTIME_DIR/./borgmatic or /run/user/$UID/./borgmatic.
|
||||
TEMPORARY_DIRECTORY_PREFIX = 'borgmatic-'
|
||||
|
||||
The "/./" is taking advantage of a Borg feature such that the part of the path before the "/./"
|
||||
does not get stored in the file path within an archive. That way, the path of the runtime
|
||||
directory can change without leaving database dumps within an archive inaccessible.
|
||||
|
||||
def replace_temporary_subdirectory_with_glob(path):
|
||||
'''
|
||||
return expand_user_in_path(
|
||||
os.path.join(
|
||||
Given an absolute temporary directory path, look for a subdirectory within it starting with the
|
||||
temporary directory prefix and replace it with an appropriate glob. For instance, given:
|
||||
|
||||
/tmp/borgmatic-aet8kn93/borgmatic
|
||||
|
||||
... replace it with:
|
||||
|
||||
/tmp/borgmatic-*/borgmatic
|
||||
|
||||
This is useful for finding previous temporary directories from prior borgmatic runs.
|
||||
'''
|
||||
return os.path.join(
|
||||
'/',
|
||||
*(
|
||||
(
|
||||
f'{TEMPORARY_DIRECTORY_PREFIX}*'
|
||||
if subdirectory.startswith(TEMPORARY_DIRECTORY_PREFIX)
|
||||
else subdirectory
|
||||
)
|
||||
for subdirectory in path.split(os.path.sep)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class Runtime_directory:
|
||||
'''
|
||||
A Python context manager for creating and cleaning up the borgmatic runtime directory used for
|
||||
storing temporary runtime data like streaming database dumps and bootstrap metadata.
|
||||
|
||||
Example use as a context manager:
|
||||
|
||||
with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
|
||||
do_something_with(borgmatic_runtime_directory)
|
||||
|
||||
For the scope of that "with" statement, the runtime directory is available. Afterwards, it
|
||||
automatically gets cleaned up as necessary.
|
||||
'''
|
||||
|
||||
def __init__(self, config, log_prefix):
|
||||
'''
|
||||
Given a configuration dict and a log prefix, determine the borgmatic runtime directory,
|
||||
creating a secure, temporary directory within it if necessary. Defaults to
|
||||
$XDG_RUNTIME_DIR/./borgmatic or $RUNTIME_DIRECTORY/./borgmatic or
|
||||
$TMPDIR/borgmatic-[random]/./borgmatic or $TEMP/borgmatic-[random]/./borgmatic or
|
||||
/tmp/borgmatic-[random]/./borgmatic where "[random]" is a randomly generated string intended
|
||||
to avoid path collisions.
|
||||
|
||||
If XDG_RUNTIME_DIR or RUNTIME_DIRECTORY is set and already ends in "/borgmatic", then don't
|
||||
tack on a second "/borgmatic" path component.
|
||||
|
||||
The "/./" is taking advantage of a Borg feature such that the part of the path before the "/./"
|
||||
does not get stored in the file path within an archive. That way, the path of the runtime
|
||||
directory can change without leaving database dumps within an archive inaccessible.
|
||||
'''
|
||||
runtime_directory = (
|
||||
config.get('user_runtime_directory')
|
||||
or os.environ.get(
|
||||
'XDG_RUNTIME_DIR',
|
||||
f'/run/user/{os.getuid()}',
|
||||
),
|
||||
'.',
|
||||
'borgmatic',
|
||||
or os.environ.get('XDG_RUNTIME_DIR') # Set by PAM on Linux.
|
||||
or os.environ.get('RUNTIME_DIRECTORY') # Set by systemd if configured.
|
||||
)
|
||||
|
||||
if runtime_directory:
|
||||
if not runtime_directory.startswith(os.path.sep):
|
||||
raise ValueError('The runtime directory must be an absolute path')
|
||||
|
||||
self.temporary_directory = None
|
||||
else:
|
||||
base_directory = os.environ.get('TMPDIR') or os.environ.get('TEMP') or '/tmp'
|
||||
|
||||
if not base_directory.startswith(os.path.sep):
|
||||
raise ValueError('The temporary directory must be an absolute path')
|
||||
|
||||
os.makedirs(base_directory, mode=0o700, exist_ok=True)
|
||||
self.temporary_directory = tempfile.TemporaryDirectory(
|
||||
prefix=TEMPORARY_DIRECTORY_PREFIX,
|
||||
dir=base_directory,
|
||||
)
|
||||
runtime_directory = self.temporary_directory.name
|
||||
|
||||
(base_path, final_directory) = os.path.split(runtime_directory.rstrip(os.path.sep))
|
||||
|
||||
self.runtime_path = expand_user_in_path(
|
||||
os.path.join(
|
||||
base_path if final_directory == 'borgmatic' else runtime_directory,
|
||||
'.', # Borg 1.4+ "slashdot" hack.
|
||||
'borgmatic',
|
||||
)
|
||||
)
|
||||
os.makedirs(self.runtime_path, mode=0o700, exist_ok=True)
|
||||
|
||||
logger.debug(f'{log_prefix}: Using runtime directory {os.path.normpath(self.runtime_path)}')
|
||||
|
||||
def __enter__(self):
|
||||
'''
|
||||
Return the borgmatic runtime path as a string.
|
||||
'''
|
||||
return self.runtime_path
|
||||
|
||||
def __exit__(self, exception, value, traceback):
|
||||
'''
|
||||
Delete any temporary directory that was created as part of initialization.
|
||||
'''
|
||||
if self.temporary_directory:
|
||||
try:
|
||||
self.temporary_directory.cleanup()
|
||||
# The cleanup() call errors if, for instance, there's still a
|
||||
# mounted filesystem within the temporary directory. There's
|
||||
# nothing we can do about that here, so swallow the error.
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def make_runtime_directory_glob(borgmatic_runtime_directory):
|
||||
'''
|
||||
Given a borgmatic runtime directory path, make a glob that would match that path, specifically
|
||||
replacing any randomly generated temporary subdirectory with "*" since such a directory's name
|
||||
changes on every borgmatic run.
|
||||
'''
|
||||
return os.path.join(
|
||||
*(
|
||||
'*' if subdirectory.startswith(TEMPORARY_DIRECTORY_PREFIX) else subdirectory
|
||||
for subdirectory in os.path.normpath(borgmatic_runtime_directory).split(os.path.sep)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -58,10 +169,9 @@ def get_borgmatic_state_directory(config):
|
||||
return expand_user_in_path(
|
||||
os.path.join(
|
||||
config.get('user_state_directory')
|
||||
or os.environ.get(
|
||||
'XDG_STATE_HOME',
|
||||
'~/.local/state',
|
||||
),
|
||||
or os.environ.get('XDG_STATE_HOME')
|
||||
or os.environ.get('STATE_DIRECTORY') # Set by systemd if configured.
|
||||
or '~/.local/state',
|
||||
'borgmatic',
|
||||
)
|
||||
)
|
||||
|
||||
+297
-10
@@ -218,8 +218,8 @@ properties:
|
||||
Path for storing temporary runtime data like streaming database
|
||||
dumps and bootstrap metadata. borgmatic automatically creates and
|
||||
uses a "borgmatic" subdirectory here. Defaults to $XDG_RUNTIME_DIR
|
||||
or /run/user/$UID.
|
||||
example: /run/user/1001/borgmatic
|
||||
or or $TMPDIR or $TEMP or /run/user/$UID.
|
||||
example: /run/user/1001
|
||||
user_state_directory:
|
||||
type: string
|
||||
description: |
|
||||
@@ -229,13 +229,6 @@ properties:
|
||||
create the check records again (and therefore re-run checks).
|
||||
Defaults to $XDG_STATE_HOME or ~/.local/state.
|
||||
example: /var/lib/borgmatic
|
||||
store_config_files:
|
||||
type: boolean
|
||||
description: |
|
||||
Store configuration files used to create a backup in the backup
|
||||
itself. Defaults to true. Changing this to false prevents "borgmatic
|
||||
bootstrap" from extracting configuration files from the backup.
|
||||
example: false
|
||||
source_directories_must_exist:
|
||||
type: boolean
|
||||
description: |
|
||||
@@ -942,6 +935,20 @@ properties:
|
||||
of them (after any action).
|
||||
example:
|
||||
- "echo Completed actions."
|
||||
bootstrap:
|
||||
type: object
|
||||
properties:
|
||||
store_config_files:
|
||||
type: boolean
|
||||
description: |
|
||||
Store configuration files used to create a backup inside the
|
||||
backup itself. Defaults to true. Changing this to false
|
||||
prevents "borgmatic bootstrap" from extracting configuration
|
||||
files from the backup.
|
||||
example: false
|
||||
description: |
|
||||
Support for the "borgmatic bootstrap" action, used to extract
|
||||
borgmatic configuration files from a backup archive.
|
||||
postgresql_databases:
|
||||
type: array
|
||||
items:
|
||||
@@ -1626,6 +1633,264 @@ properties:
|
||||
example:
|
||||
- start
|
||||
- finish
|
||||
pushover:
|
||||
type: object
|
||||
required: ['token', 'user']
|
||||
additionalProperties: false
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
description: |
|
||||
Your application's API token.
|
||||
example: 7ms6TXHpTokTou2P6x4SodDeentHRa
|
||||
user:
|
||||
type: string
|
||||
description: |
|
||||
Your user/group key (or that of your target user), viewable
|
||||
when logged into your dashboard: often referred to as
|
||||
USER_KEY in Pushover documentation and code examples.
|
||||
example: hwRwoWsXMBWwgrSecfa9EfPey55WSN
|
||||
start:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
description: |
|
||||
Message to be sent to the user or group. If omitted
|
||||
the default is the name of the state.
|
||||
example: A backup job has started.
|
||||
priority:
|
||||
type: integer
|
||||
description: |
|
||||
A value of -2, -1, 0 (default), 1 or 2 that
|
||||
indicates the message priority.
|
||||
example: 0
|
||||
expire:
|
||||
type: integer
|
||||
description: |
|
||||
How many seconds your notification will continue
|
||||
to be retried (every retry seconds). Defaults to
|
||||
600. This settings only applies to priority 2
|
||||
notifications.
|
||||
example: 600
|
||||
retry:
|
||||
type: integer
|
||||
description: |
|
||||
The retry parameter specifies how often
|
||||
(in seconds) the Pushover servers will send the
|
||||
same notification to the user. Defaults to 30. This
|
||||
settings only applies to priority 2 notifications.
|
||||
example: 30
|
||||
device:
|
||||
type: string
|
||||
description: |
|
||||
The name of one of your devices to send just to
|
||||
that device instead of all devices.
|
||||
example: pixel8
|
||||
html:
|
||||
type: boolean
|
||||
description: |
|
||||
Set to True to enable HTML parsing of the message.
|
||||
Set to False for plain text.
|
||||
example: True
|
||||
sound:
|
||||
type: string
|
||||
description: |
|
||||
The name of a supported sound to override your
|
||||
default sound choice. All options can be found
|
||||
here: https://pushover.net/api#sounds
|
||||
example: bike
|
||||
title:
|
||||
type: string
|
||||
description: |
|
||||
Your message's title, otherwise your app's name is
|
||||
used.
|
||||
example: A backup job has started.
|
||||
ttl:
|
||||
type: integer
|
||||
description: |
|
||||
The number of seconds that the message will live,
|
||||
before being deleted automatically. The ttl
|
||||
parameter is ignored for messages with a priority.
|
||||
value of 2.
|
||||
example: 3600
|
||||
url:
|
||||
type: string
|
||||
description: |
|
||||
A supplementary URL to show with your message.
|
||||
example: https://pushover.net/apps/xxxxx-borgbackup
|
||||
url_title:
|
||||
type: string
|
||||
description: |
|
||||
A title for the URL specified as the url parameter,
|
||||
otherwise just the URL is shown.
|
||||
example: Pushover Link
|
||||
finish:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
description: |
|
||||
Message to be sent to the user or group. If omitted
|
||||
the default is the name of the state.
|
||||
example: A backup job has finished.
|
||||
priority:
|
||||
type: integer
|
||||
description: |
|
||||
A value of -2, -1, 0 (default), 1 or 2 that
|
||||
indicates the message priority.
|
||||
example: 0
|
||||
expire:
|
||||
type: integer
|
||||
description: |
|
||||
How many seconds your notification will continue
|
||||
to be retried (every retry seconds). Defaults to
|
||||
600. This settings only applies to priority 2
|
||||
notifications.
|
||||
example: 600
|
||||
retry:
|
||||
type: integer
|
||||
description: |
|
||||
The retry parameter specifies how often
|
||||
(in seconds) the Pushover servers will send the
|
||||
same notification to the user. Defaults to 30. This
|
||||
settings only applies to priority 2 notifications.
|
||||
example: 30
|
||||
device:
|
||||
type: string
|
||||
description: |
|
||||
The name of one of your devices to send just to
|
||||
that device instead of all devices.
|
||||
example: pixel8
|
||||
html:
|
||||
type: boolean
|
||||
description: |
|
||||
Set to True to enable HTML parsing of the message.
|
||||
Set to False for plain text.
|
||||
example: True
|
||||
sound:
|
||||
type: string
|
||||
description: |
|
||||
The name of a supported sound to override your
|
||||
default sound choice. All options can be found
|
||||
here: https://pushover.net/api#sounds
|
||||
example: bike
|
||||
title:
|
||||
type: string
|
||||
description: |
|
||||
Your message's title, otherwise your app's name is
|
||||
used.
|
||||
example: A backup job has started.
|
||||
ttl:
|
||||
type: integer
|
||||
description: |
|
||||
The number of seconds that the message will live,
|
||||
before being deleted automatically. The ttl
|
||||
parameter is ignored for messages with a priority.
|
||||
value of 2.
|
||||
example: 3600
|
||||
url:
|
||||
type: string
|
||||
description: |
|
||||
A supplementary URL to show with your message.
|
||||
example: https://pushover.net/apps/xxxxx-borgbackup
|
||||
url_title:
|
||||
type: string
|
||||
description: |
|
||||
A title for the URL specified as the url parameter,
|
||||
otherwise just the URL is shown.
|
||||
example: Pushover Link
|
||||
fail:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
description: |
|
||||
Message to be sent to the user or group. If omitted
|
||||
the default is the name of the state.
|
||||
example: A backup job has failed.
|
||||
priority:
|
||||
type: integer
|
||||
description: |
|
||||
A value of -2, -1, 0 (default), 1 or 2 that
|
||||
indicates the message priority.
|
||||
example: 0
|
||||
expire:
|
||||
type: integer
|
||||
description: |
|
||||
How many seconds your notification will continue
|
||||
to be retried (every retry seconds). Defaults to
|
||||
600. This settings only applies to priority 2
|
||||
notifications.
|
||||
example: 600
|
||||
retry:
|
||||
type: integer
|
||||
description: |
|
||||
The retry parameter specifies how often
|
||||
(in seconds) the Pushover servers will send the
|
||||
same notification to the user. Defaults to 30. This
|
||||
settings only applies to priority 2 notifications.
|
||||
example: 30
|
||||
device:
|
||||
type: string
|
||||
description: |
|
||||
The name of one of your devices to send just to
|
||||
that device instead of all devices.
|
||||
example: pixel8
|
||||
html:
|
||||
type: boolean
|
||||
description: |
|
||||
Set to True to enable HTML parsing of the message.
|
||||
Set to False for plain text.
|
||||
example: True
|
||||
sound:
|
||||
type: string
|
||||
description: |
|
||||
The name of a supported sound to override your
|
||||
default sound choice. All options can be found
|
||||
here: https://pushover.net/api#sounds
|
||||
example: bike
|
||||
title:
|
||||
type: string
|
||||
description: |
|
||||
Your message's title, otherwise your app's name is
|
||||
used.
|
||||
example: A backup job has started.
|
||||
ttl:
|
||||
type: integer
|
||||
description: |
|
||||
The number of seconds that the message will live,
|
||||
before being deleted automatically. The ttl
|
||||
parameter is ignored for messages with a priority.
|
||||
value of 2.
|
||||
example: 3600
|
||||
url:
|
||||
type: string
|
||||
description: |
|
||||
A supplementary URL to show with your message.
|
||||
example: https://pushover.net/apps/xxxxx-borgbackup
|
||||
url_title:
|
||||
type: string
|
||||
description: |
|
||||
A title for the URL specified as the url parameter,
|
||||
otherwise just the URL is shown.
|
||||
example: Pushover Link
|
||||
states:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- start
|
||||
- finish
|
||||
- fail
|
||||
uniqueItems: true
|
||||
description: |
|
||||
List of one or more monitoring states to ping for: "start",
|
||||
"finish", and/or "fail". Defaults to pinging for failure
|
||||
only.
|
||||
example:
|
||||
- start
|
||||
- finish
|
||||
zabbix:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
@@ -1997,7 +2262,29 @@ properties:
|
||||
config: "__config"
|
||||
hostname: "__hostname"
|
||||
description: |
|
||||
Configuration for a monitoring integration with Grafana loki. You
|
||||
Configuration for a monitoring integration with Grafana Loki. You
|
||||
can send the logs to a self-hosted instance or create an account at
|
||||
https://grafana.com/auth/sign-up/create-user. See borgmatic
|
||||
monitoring documentation for details.
|
||||
|
||||
zfs:
|
||||
type: ["object", "null"]
|
||||
additionalProperties: false
|
||||
properties:
|
||||
zfs_command:
|
||||
type: string
|
||||
description: |
|
||||
Command to use instead of "zfs".
|
||||
example: /usr/local/bin/zfs
|
||||
mount_command:
|
||||
type: string
|
||||
description: |
|
||||
Command to use instead of "mount".
|
||||
example: /usr/local/bin/mount
|
||||
umount_command:
|
||||
type: string
|
||||
description: |
|
||||
Command to use instead of "umount".
|
||||
example: /usr/local/bin/umount
|
||||
description: |
|
||||
Configuration for integration with the ZFS filesystem.
|
||||
|
||||
@@ -199,26 +199,3 @@ def guard_configuration_contains_repository(repository, configurations):
|
||||
|
||||
if count == 0:
|
||||
raise ValueError(f'Repository "{repository}" not found in configuration files')
|
||||
|
||||
|
||||
def guard_single_repository_selected(repository, configurations):
|
||||
'''
|
||||
Given a repository path and a dict mapping from config filename to corresponding parsed config
|
||||
dict, ensure either a single repository exists across all configuration files or a repository
|
||||
path was given.
|
||||
'''
|
||||
if repository:
|
||||
return
|
||||
|
||||
count = len(
|
||||
tuple(
|
||||
config_repository
|
||||
for config in configurations.values()
|
||||
for config_repository in config['repositories']
|
||||
)
|
||||
)
|
||||
|
||||
if count != 1:
|
||||
raise ValueError(
|
||||
"Can't determine which repository to use. Use --repository to disambiguate"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import glob
|
||||
import importlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
import borgmatic.config.paths
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def use_streaming(hook_config, config, log_prefix): # pragma: no cover
|
||||
'''
|
||||
Return whether dump streaming is used for this hook. (Spoiler: It isn't.)
|
||||
'''
|
||||
return False
|
||||
|
||||
|
||||
def dump_data_sources(
|
||||
hook_config,
|
||||
config,
|
||||
log_prefix,
|
||||
config_paths,
|
||||
borgmatic_runtime_directory,
|
||||
source_directories,
|
||||
dry_run,
|
||||
):
|
||||
'''
|
||||
Given a bootstrap configuration dict, a configuration dict, a log prefix, the borgmatic
|
||||
configuration file paths, the borgmatic runtime directory, the configured source directories,
|
||||
and whether this is a dry run, create a borgmatic manifest file to store the paths of the
|
||||
configuration files used to create the archive. But skip this if the bootstrap
|
||||
store_config_files option is False or if this is a dry run.
|
||||
|
||||
Return an empty sequence, since there are no ongoing dump processes from this hook.
|
||||
'''
|
||||
if hook_config.get('store_config_files') is False:
|
||||
return []
|
||||
|
||||
borgmatic_manifest_path = os.path.join(
|
||||
borgmatic_runtime_directory, 'bootstrap', 'manifest.json'
|
||||
)
|
||||
|
||||
if dry_run:
|
||||
return []
|
||||
|
||||
os.makedirs(os.path.dirname(borgmatic_manifest_path), exist_ok=True)
|
||||
|
||||
with open(borgmatic_manifest_path, 'w') as manifest_file:
|
||||
json.dump(
|
||||
{
|
||||
'borgmatic_version': importlib.metadata.version('borgmatic'),
|
||||
'config_paths': config_paths,
|
||||
},
|
||||
manifest_file,
|
||||
)
|
||||
|
||||
source_directories.extend(config_paths)
|
||||
source_directories.append(os.path.join(borgmatic_runtime_directory, 'bootstrap'))
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def remove_data_source_dumps(hook_config, config, log_prefix, borgmatic_runtime_directory, dry_run):
|
||||
'''
|
||||
Given a bootstrap configuration dict, a configuration dict, a log prefix, the borgmatic runtime
|
||||
directory, and whether this is a dry run, then remove the manifest file created above. If this
|
||||
is a dry run, then don't actually remove anything.
|
||||
'''
|
||||
dry_run_label = ' (dry run; not actually removing anything)' if dry_run else ''
|
||||
|
||||
manifest_glob = os.path.join(
|
||||
borgmatic.config.paths.replace_temporary_subdirectory_with_glob(
|
||||
os.path.normpath(borgmatic_runtime_directory),
|
||||
),
|
||||
'bootstrap',
|
||||
)
|
||||
logger.debug(
|
||||
f'{log_prefix}: Looking for bootstrap manifest files to remove in {manifest_glob}{dry_run_label}'
|
||||
)
|
||||
|
||||
for manifest_directory in glob.glob(manifest_glob):
|
||||
manifest_file_path = os.path.join(manifest_directory, 'manifest.json')
|
||||
logger.debug(
|
||||
f'{log_prefix}: Removing bootstrap manifest at {manifest_file_path}{dry_run_label}'
|
||||
)
|
||||
|
||||
if dry_run:
|
||||
continue
|
||||
|
||||
try:
|
||||
os.remove(manifest_file_path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
try:
|
||||
os.rmdir(manifest_directory)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def make_data_source_dump_patterns(
|
||||
hook_config, config, log_prefix, borgmatic_runtime_directory, name=None
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Restores are implemented via the separate, purpose-specific "bootstrap" action rather than the
|
||||
generic "restore".
|
||||
'''
|
||||
return ()
|
||||
|
||||
|
||||
def restore_data_source_dump(
|
||||
hook_config,
|
||||
config,
|
||||
log_prefix,
|
||||
data_source,
|
||||
dry_run,
|
||||
extract_process,
|
||||
connection_params,
|
||||
borgmatic_runtime_directory,
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Restores are implemented via the separate, purpose-specific "bootstrap" action rather than the
|
||||
generic "restore".
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
@@ -2,6 +2,7 @@ import logging
|
||||
|
||||
from borgmatic.hooks import (
|
||||
apprise,
|
||||
bootstrap,
|
||||
cronhub,
|
||||
cronitor,
|
||||
healthchecks,
|
||||
@@ -12,15 +13,18 @@ from borgmatic.hooks import (
|
||||
ntfy,
|
||||
pagerduty,
|
||||
postgresql,
|
||||
pushover,
|
||||
sqlite,
|
||||
uptimekuma,
|
||||
zabbix,
|
||||
zfs,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
HOOK_NAME_TO_MODULE = {
|
||||
'apprise': apprise,
|
||||
'bootstrap': bootstrap,
|
||||
'cronhub': cronhub,
|
||||
'cronitor': cronitor,
|
||||
'healthchecks': healthchecks,
|
||||
@@ -31,9 +35,11 @@ HOOK_NAME_TO_MODULE = {
|
||||
'ntfy': ntfy,
|
||||
'pagerduty': pagerduty,
|
||||
'postgresql_databases': postgresql,
|
||||
'pushover': pushover,
|
||||
'sqlite_databases': sqlite,
|
||||
'uptime_kuma': uptimekuma,
|
||||
'zabbix': zabbix,
|
||||
'zfs': zfs,
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +53,7 @@ def call_hook(function_name, config, log_prefix, hook_name, *args, **kwargs):
|
||||
Raise AttributeError if the function name is not found in the module.
|
||||
Raise anything else that the called function raises.
|
||||
'''
|
||||
hook_config = config.get(hook_name, {})
|
||||
hook_config = config.get(hook_name) or {}
|
||||
|
||||
try:
|
||||
module = HOOK_NAME_TO_MODULE[hook_name]
|
||||
@@ -75,7 +81,7 @@ def call_hooks(function_name, config, log_prefix, hook_names, *args, **kwargs):
|
||||
return {
|
||||
hook_name: call_hook(function_name, config, log_prefix, hook_name, *args, **kwargs)
|
||||
for hook_name in hook_names
|
||||
if config.get(hook_name)
|
||||
if hook_name in config
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,11 +6,13 @@ import shutil
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DATA_SOURCE_HOOK_NAMES = (
|
||||
'bootstrap',
|
||||
'mariadb_databases',
|
||||
'mysql_databases',
|
||||
'mongodb_databases',
|
||||
'postgresql_databases',
|
||||
'sqlite_databases',
|
||||
'zfs',
|
||||
)
|
||||
|
||||
|
||||
|
||||
+48
-28
@@ -14,15 +14,11 @@ from borgmatic.hooks import dump
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def make_dump_path(config, base_directory=None): # pragma: no cover
|
||||
def make_dump_path(base_directory): # pragma: no cover
|
||||
'''
|
||||
Given a configuration dict and an optional base directory, make the corresponding dump path. If
|
||||
a base directory isn't provided, use the borgmatic runtime directory.
|
||||
Given a base directory, make the corresponding dump path.
|
||||
'''
|
||||
return dump.make_data_source_dump_path(
|
||||
base_directory or borgmatic.config.paths.get_borgmatic_runtime_directory(config),
|
||||
'mariadb_databases',
|
||||
)
|
||||
return dump.make_data_source_dump_path(base_directory, 'mariadb_databases')
|
||||
|
||||
|
||||
SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys')
|
||||
@@ -126,15 +122,24 @@ def use_streaming(databases, config, log_prefix):
|
||||
return any(databases)
|
||||
|
||||
|
||||
def dump_data_sources(databases, config, log_prefix, dry_run):
|
||||
def dump_data_sources(
|
||||
databases,
|
||||
config,
|
||||
log_prefix,
|
||||
config_paths,
|
||||
borgmatic_runtime_directory,
|
||||
source_directories,
|
||||
dry_run,
|
||||
):
|
||||
'''
|
||||
Dump the given MariaDB databases to a named pipe. The databases are supplied as a sequence of
|
||||
dicts, one dict describing each database as per the configuration schema. Use the given
|
||||
configuration dict to construct the destination path and the given log prefix in any log
|
||||
entries.
|
||||
borgmatic runtime directory to construct the destination path and the given log prefix in any
|
||||
log entries.
|
||||
|
||||
Return a sequence of subprocess.Popen instances for the dump processes ready to spew to a named
|
||||
pipe. But if this is a dry run, then don't actually dump anything and return an empty sequence.
|
||||
Also append the given source directories with the parent directory of the database dumps.
|
||||
'''
|
||||
dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
|
||||
processes = []
|
||||
@@ -142,7 +147,7 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
|
||||
logger.info(f'{log_prefix}: Dumping MariaDB databases{dry_run_label}')
|
||||
|
||||
for database in databases:
|
||||
dump_path = make_dump_path(config)
|
||||
dump_path = make_dump_path(borgmatic_runtime_directory)
|
||||
extra_environment = {'MYSQL_PWD': database['password']} if 'password' in database else None
|
||||
dump_database_names = database_names_to_dump(
|
||||
database, extra_environment, log_prefix, dry_run
|
||||
@@ -182,46 +187,61 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
|
||||
)
|
||||
)
|
||||
|
||||
if not dry_run:
|
||||
source_directories.append(os.path.join(borgmatic_runtime_directory, 'mariadb_databases'))
|
||||
|
||||
return [process for process in processes if process]
|
||||
|
||||
|
||||
def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma: no cover
|
||||
def remove_data_source_dumps(
|
||||
databases, config, log_prefix, borgmatic_runtime_directory, dry_run
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Remove all database dump files for this hook regardless of the given databases. Use the given
|
||||
configuration dict to construct the destination path and the log prefix in any log entries. If
|
||||
this is a dry run, then don't actually remove anything.
|
||||
Remove all database dump files for this hook regardless of the given databases. Use the
|
||||
borgmatic_runtime_directory to construct the destination path and the log prefix in any log
|
||||
entries. If this is a dry run, then don't actually remove anything.
|
||||
'''
|
||||
dump.remove_data_source_dumps(make_dump_path(config), 'MariaDB', log_prefix, dry_run)
|
||||
dump.remove_data_source_dumps(
|
||||
make_dump_path(borgmatic_runtime_directory), 'MariaDB', log_prefix, dry_run
|
||||
)
|
||||
|
||||
|
||||
def make_data_source_dump_patterns(databases, config, log_prefix, name=None): # pragma: no cover
|
||||
def make_data_source_dump_patterns(
|
||||
databases, config, log_prefix, borgmatic_runtime_directory, name=None
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Given a sequence of configurations dicts, a configuration dict, a prefix to log with, and a
|
||||
database name to match, return the corresponding glob patterns to match the database dump in an
|
||||
archive.
|
||||
Given a sequence of configurations dicts, a configuration dict, a prefix to log with, the
|
||||
borgmatic runtime directory, and a database name to match, return the corresponding glob
|
||||
patterns to match the database dump in an archive.
|
||||
'''
|
||||
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
||||
|
||||
return (
|
||||
dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(config, 'borgmatic'), name, hostname='*'
|
||||
make_dump_path(borgmatic_runtime_directory), name, hostname='*'
|
||||
),
|
||||
dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*'),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(config, borgmatic_source_directory), name, hostname='*'
|
||||
make_dump_path(borgmatic_source_directory), name, hostname='*'
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def restore_data_source_dump(
|
||||
hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params
|
||||
hook_config,
|
||||
config,
|
||||
log_prefix,
|
||||
data_source,
|
||||
dry_run,
|
||||
extract_process,
|
||||
connection_params,
|
||||
borgmatic_runtime_directory,
|
||||
):
|
||||
'''
|
||||
Restore a database from the given extract stream. The database is supplied as a data source
|
||||
configuration dict, but the given hook configuration is ignored. The given configuration dict is
|
||||
used to construct the destination path, and the given log prefix is used for any log entries. If
|
||||
this is a dry run, then don't actually restore anything. Trigger the given active extract
|
||||
process (an instance of subprocess.Popen) to produce output to consume.
|
||||
configuration dict, but the given hook configuration is ignored. The given log prefix is used
|
||||
for any log entries. If this is a dry run, then don't actually restore anything. Trigger the
|
||||
given active extract process (an instance of subprocess.Popen) to produce output to consume.
|
||||
'''
|
||||
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
|
||||
hostname = connection_params['hostname'] or data_source.get(
|
||||
|
||||
+50
-25
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import os
|
||||
import shlex
|
||||
|
||||
import borgmatic.config.paths
|
||||
@@ -8,15 +9,11 @@ from borgmatic.hooks import dump
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def make_dump_path(config, base_directory=None): # pragma: no cover
|
||||
def make_dump_path(base_directory): # pragma: no cover
|
||||
'''
|
||||
Given a configuration dict and an optional base directory, make the corresponding dump path. If
|
||||
a base directory isn't provided, use the borgmatic runtime directory.
|
||||
Given a base directory, make the corresponding dump path.
|
||||
'''
|
||||
return dump.make_data_source_dump_path(
|
||||
base_directory or borgmatic.config.paths.get_borgmatic_runtime_directory(config),
|
||||
'mongodb_databases',
|
||||
)
|
||||
return dump.make_data_source_dump_path(base_directory, 'mongodb_databases')
|
||||
|
||||
|
||||
def use_streaming(databases, config, log_prefix):
|
||||
@@ -27,14 +24,24 @@ def use_streaming(databases, config, log_prefix):
|
||||
return any(database.get('format') != 'directory' for database in databases)
|
||||
|
||||
|
||||
def dump_data_sources(databases, config, log_prefix, dry_run):
|
||||
def dump_data_sources(
|
||||
databases,
|
||||
config,
|
||||
log_prefix,
|
||||
config_paths,
|
||||
borgmatic_runtime_directory,
|
||||
source_directories,
|
||||
dry_run,
|
||||
):
|
||||
'''
|
||||
Dump the given MongoDB databases to a named pipe. The databases are supplied as a sequence of
|
||||
dicts, one dict describing each database as per the configuration schema. Use the configuration
|
||||
dict to construct the destination path and the given log prefix in any log entries.
|
||||
dicts, one dict describing each database as per the configuration schema. Use the borgmatic
|
||||
runtime directory to construct the destination path (used for the directory format and the given
|
||||
log prefix in any log entries.
|
||||
|
||||
Return a sequence of subprocess.Popen instances for the dump processes ready to spew to a named
|
||||
pipe. But if this is a dry run, then don't actually dump anything and return an empty sequence.
|
||||
Also append the given source directories with the parent directory of the database dumps.
|
||||
'''
|
||||
dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
|
||||
|
||||
@@ -44,7 +51,7 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
|
||||
for database in databases:
|
||||
name = database['name']
|
||||
dump_filename = dump.make_data_source_dump_filename(
|
||||
make_dump_path(config), name, database.get('hostname')
|
||||
make_dump_path(borgmatic_runtime_directory), name, database.get('hostname')
|
||||
)
|
||||
dump_format = database.get('format', 'archive')
|
||||
|
||||
@@ -63,6 +70,9 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
|
||||
dump.create_named_pipe_for_dump(dump_filename)
|
||||
processes.append(execute_command(command, shell=True, run_to_completion=False))
|
||||
|
||||
if not dry_run:
|
||||
source_directories.append(os.path.join(borgmatic_runtime_directory, 'mongodb_databases'))
|
||||
|
||||
return processes
|
||||
|
||||
|
||||
@@ -94,36 +104,49 @@ def build_dump_command(database, dump_filename, dump_format):
|
||||
)
|
||||
|
||||
|
||||
def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma: no cover
|
||||
def remove_data_source_dumps(
|
||||
databases, config, log_prefix, borgmatic_runtime_directory, dry_run
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Remove all database dump files for this hook regardless of the given databases. Use the log
|
||||
prefix in any log entries. Use the given configuration dict to construct the destination path.
|
||||
If this is a dry run, then don't actually remove anything.
|
||||
Remove all database dump files for this hook regardless of the given databases. Use the
|
||||
borgmatic_runtime_directory to construct the destination path and the log prefix in any log
|
||||
entries. If this is a dry run, then don't actually remove anything.
|
||||
'''
|
||||
dump.remove_data_source_dumps(make_dump_path(config), 'MongoDB', log_prefix, dry_run)
|
||||
dump.remove_data_source_dumps(
|
||||
make_dump_path(borgmatic_runtime_directory), 'MongoDB', log_prefix, dry_run
|
||||
)
|
||||
|
||||
|
||||
def make_data_source_dump_patterns(databases, config, log_prefix, name=None): # pragma: no cover
|
||||
def make_data_source_dump_patterns(
|
||||
databases, config, log_prefix, borgmatic_runtime_directory, name=None
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Given a sequence of database configurations dicts, a configuration dict, a prefix to log with,
|
||||
and a database name to match, return the corresponding glob patterns to match the database dump
|
||||
in an archive.
|
||||
Given a sequence of configurations dicts, a configuration dict, a prefix to log with, the
|
||||
borgmatic runtime directory, and a database name to match, return the corresponding glob
|
||||
patterns to match the database dump in an archive.
|
||||
'''
|
||||
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
||||
|
||||
return (
|
||||
dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(config, 'borgmatic'), name, hostname='*'
|
||||
make_dump_path(borgmatic_runtime_directory), name, hostname='*'
|
||||
),
|
||||
dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*'),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(config, borgmatic_source_directory), name, hostname='*'
|
||||
make_dump_path(borgmatic_source_directory), name, hostname='*'
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def restore_data_source_dump(
|
||||
hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params
|
||||
hook_config,
|
||||
config,
|
||||
log_prefix,
|
||||
data_source,
|
||||
dry_run,
|
||||
extract_process,
|
||||
connection_params,
|
||||
borgmatic_runtime_directory,
|
||||
):
|
||||
'''
|
||||
Restore a database from the given extract stream. The database is supplied as a data source
|
||||
@@ -137,7 +160,9 @@ def restore_data_source_dump(
|
||||
'''
|
||||
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
|
||||
dump_filename = dump.make_data_source_dump_filename(
|
||||
make_dump_path(config), data_source['name'], data_source.get('hostname')
|
||||
make_dump_path(borgmatic_runtime_directory),
|
||||
data_source['name'],
|
||||
data_source.get('hostname'),
|
||||
)
|
||||
restore_command = build_restore_command(
|
||||
extract_process, data_source, dump_filename, connection_params
|
||||
|
||||
@@ -8,6 +8,7 @@ MONITOR_HOOK_NAMES = (
|
||||
'loki',
|
||||
'ntfy',
|
||||
'pagerduty',
|
||||
'pushover',
|
||||
'uptime_kuma',
|
||||
'zabbix',
|
||||
)
|
||||
|
||||
+48
-27
@@ -14,15 +14,11 @@ from borgmatic.hooks import dump
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def make_dump_path(config, base_directory=None): # pragma: no cover
|
||||
def make_dump_path(base_directory): # pragma: no cover
|
||||
'''
|
||||
Given a configuration dict and an optional base directory, make the corresponding dump path. If
|
||||
a base directory isn't provided, use the borgmatic runtime directory.
|
||||
Given a base directory, make the corresponding dump path.
|
||||
'''
|
||||
return dump.make_data_source_dump_path(
|
||||
base_directory or borgmatic.config.paths.get_borgmatic_runtime_directory(config),
|
||||
'mysql_databases',
|
||||
)
|
||||
return dump.make_data_source_dump_path(base_directory, 'mysql_databases')
|
||||
|
||||
|
||||
SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys')
|
||||
@@ -125,14 +121,24 @@ def use_streaming(databases, config, log_prefix):
|
||||
return any(databases)
|
||||
|
||||
|
||||
def dump_data_sources(databases, config, log_prefix, dry_run):
|
||||
def dump_data_sources(
|
||||
databases,
|
||||
config,
|
||||
log_prefix,
|
||||
config_paths,
|
||||
borgmatic_runtime_directory,
|
||||
source_directories,
|
||||
dry_run,
|
||||
):
|
||||
'''
|
||||
Dump the given MySQL/MariaDB databases to a named pipe. The databases are supplied as a sequence
|
||||
of dicts, one dict describing each database as per the configuration schema. Use the given
|
||||
configuration dict to construct the destination path and the given log prefix in any log entries.
|
||||
borgmatic runtime directory to construct the destination path and the given log prefix in any
|
||||
log entries.
|
||||
|
||||
Return a sequence of subprocess.Popen instances for the dump processes ready to spew to a named
|
||||
pipe. But if this is a dry run, then don't actually dump anything and return an empty sequence.
|
||||
Also append the given source directories with the parent directory of the database dumps.
|
||||
'''
|
||||
dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
|
||||
processes = []
|
||||
@@ -140,7 +146,7 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
|
||||
logger.info(f'{log_prefix}: Dumping MySQL databases{dry_run_label}')
|
||||
|
||||
for database in databases:
|
||||
dump_path = make_dump_path(config)
|
||||
dump_path = make_dump_path(borgmatic_runtime_directory)
|
||||
extra_environment = {'MYSQL_PWD': database['password']} if 'password' in database else None
|
||||
dump_database_names = database_names_to_dump(
|
||||
database, extra_environment, log_prefix, dry_run
|
||||
@@ -180,46 +186,61 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
|
||||
)
|
||||
)
|
||||
|
||||
if not dry_run:
|
||||
source_directories.append(os.path.join(borgmatic_runtime_directory, 'mysql_databases'))
|
||||
|
||||
return [process for process in processes if process]
|
||||
|
||||
|
||||
def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma: no cover
|
||||
def remove_data_source_dumps(
|
||||
databases, config, log_prefix, borgmatic_runtime_directory, dry_run
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Remove all database dump files for this hook regardless of the given databases. Use the given
|
||||
configuration dict to construct the destination path and the log prefix in any log entries. If
|
||||
this is a dry run, then don't actually remove anything.
|
||||
Remove all database dump files for this hook regardless of the given databases. Use the
|
||||
borgmatic runtime directory to construct the destination path and the log prefix in any log
|
||||
entries. If this is a dry run, then don't actually remove anything.
|
||||
'''
|
||||
dump.remove_data_source_dumps(make_dump_path(config), 'MySQL', log_prefix, dry_run)
|
||||
dump.remove_data_source_dumps(
|
||||
make_dump_path(borgmatic_runtime_directory), 'MySQL', log_prefix, dry_run
|
||||
)
|
||||
|
||||
|
||||
def make_data_source_dump_patterns(databases, config, log_prefix, name=None): # pragma: no cover
|
||||
def make_data_source_dump_patterns(
|
||||
databases, config, log_prefix, borgmatic_runtime_directory, name=None
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Given a sequence of configurations dicts, a configuration dict, a prefix to log with, and a
|
||||
database name to match, return the corresponding glob patterns to match the database dump in an
|
||||
archive.
|
||||
Given a sequence of configurations dicts, a configuration dict, a prefix to log with, the
|
||||
borgmatic runtime directory, and a database name to match, return the corresponding glob
|
||||
patterns to match the database dump in an archive.
|
||||
'''
|
||||
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
||||
|
||||
return (
|
||||
dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(config, 'borgmatic'), name, hostname='*'
|
||||
make_dump_path(borgmatic_runtime_directory), name, hostname='*'
|
||||
),
|
||||
dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*'),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(config, borgmatic_source_directory), name, hostname='*'
|
||||
make_dump_path(borgmatic_source_directory), name, hostname='*'
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def restore_data_source_dump(
|
||||
hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params
|
||||
hook_config,
|
||||
config,
|
||||
log_prefix,
|
||||
data_source,
|
||||
dry_run,
|
||||
extract_process,
|
||||
connection_params,
|
||||
borgmatic_runtime_directory,
|
||||
):
|
||||
'''
|
||||
Restore a database from the given extract stream. The database is supplied as a data source
|
||||
configuration dict, but the given hook configuration is ignored. The given configuration dict is
|
||||
used to construct the destination path, and the given log prefix is used for any log entries. If
|
||||
this is a dry run, then don't actually restore anything. Trigger the given active extract
|
||||
process (an instance of subprocess.Popen) to produce output to consume.
|
||||
configuration dict, but the given hook configuration is ignored. The given log prefix is used
|
||||
for any log entries. If this is a dry run, then don't actually restore anything. Trigger the
|
||||
given active extract process (an instance of subprocess.Popen) to produce output to consume.
|
||||
'''
|
||||
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
|
||||
hostname = connection_params['hostname'] or data_source.get(
|
||||
|
||||
@@ -16,15 +16,11 @@ from borgmatic.hooks import dump
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def make_dump_path(config, base_directory=None): # pragma: no cover
|
||||
def make_dump_path(base_directory): # pragma: no cover
|
||||
'''
|
||||
Given a configuration dict and an optional base directory, make the corresponding dump path. If
|
||||
a base directory isn't provided, use the borgmatic runtime directory.
|
||||
Given a base directory, make the corresponding dump path.
|
||||
'''
|
||||
return dump.make_data_source_dump_path(
|
||||
base_directory or borgmatic.config.paths.get_borgmatic_runtime_directory(config),
|
||||
'postgresql_databases',
|
||||
)
|
||||
return dump.make_data_source_dump_path(base_directory, 'postgresql_databases')
|
||||
|
||||
|
||||
def make_extra_environment(database, restore_connection_params=None):
|
||||
@@ -108,15 +104,24 @@ def use_streaming(databases, config, log_prefix):
|
||||
return any(database.get('format') != 'directory' for database in databases)
|
||||
|
||||
|
||||
def dump_data_sources(databases, config, log_prefix, dry_run):
|
||||
def dump_data_sources(
|
||||
databases,
|
||||
config,
|
||||
log_prefix,
|
||||
config_paths,
|
||||
borgmatic_runtime_directory,
|
||||
source_directories,
|
||||
dry_run,
|
||||
):
|
||||
'''
|
||||
Dump the given PostgreSQL databases to a named pipe. The databases are supplied as a sequence of
|
||||
dicts, one dict describing each database as per the configuration schema. Use the given
|
||||
configuration dict to construct the destination path and the given log prefix in any log
|
||||
entries.
|
||||
borgmatic runtime directory to construct the destination path and the given log prefix in any
|
||||
log entries.
|
||||
|
||||
Return a sequence of subprocess.Popen instances for the dump processes ready to spew to a named
|
||||
pipe. But if this is a dry run, then don't actually dump anything and return an empty sequence.
|
||||
Also append the given source directories with the parent directory of the database dumps.
|
||||
|
||||
Raise ValueError if the databases to dump cannot be determined.
|
||||
'''
|
||||
@@ -127,7 +132,7 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
|
||||
|
||||
for database in databases:
|
||||
extra_environment = make_extra_environment(database)
|
||||
dump_path = make_dump_path(config)
|
||||
dump_path = make_dump_path(borgmatic_runtime_directory)
|
||||
dump_database_names = database_names_to_dump(
|
||||
database, extra_environment, log_prefix, dry_run
|
||||
)
|
||||
@@ -207,46 +212,63 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
|
||||
)
|
||||
)
|
||||
|
||||
if not dry_run:
|
||||
source_directories.append(os.path.join(borgmatic_runtime_directory, 'postgresql_databases'))
|
||||
|
||||
return processes
|
||||
|
||||
|
||||
def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma: no cover
|
||||
def remove_data_source_dumps(
|
||||
databases, config, log_prefix, borgmatic_runtime_directory, dry_run
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Remove all database dump files for this hook regardless of the given databases. Use the given
|
||||
configuration dict to construct the destination path and the log prefix in any log entries. If
|
||||
this is a dry run, then don't actually remove anything.
|
||||
Remove all database dump files for this hook regardless of the given databases. Use the
|
||||
borgmatic runtime directory to construct the destination path and the log prefix in any log
|
||||
entries. If this is a dry run, then don't actually remove anything.
|
||||
'''
|
||||
dump.remove_data_source_dumps(make_dump_path(config), 'PostgreSQL', log_prefix, dry_run)
|
||||
dump.remove_data_source_dumps(
|
||||
make_dump_path(borgmatic_runtime_directory), 'PostgreSQL', log_prefix, dry_run
|
||||
)
|
||||
|
||||
|
||||
def make_data_source_dump_patterns(databases, config, log_prefix, name=None): # pragma: no cover
|
||||
def make_data_source_dump_patterns(
|
||||
databases, config, log_prefix, borgmatic_runtime_directory, name=None
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Given a sequence of configurations dicts, a configuration dict, a prefix to log with, and a
|
||||
database name to match, return the corresponding glob patterns to match the database dump in an
|
||||
archive.
|
||||
Given a sequence of configurations dicts, a configuration dict, a prefix to log with, the
|
||||
borgmatic runtime directory, and a database name to match, return the corresponding glob
|
||||
patterns to match the database dump in an archive.
|
||||
'''
|
||||
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
||||
|
||||
return (
|
||||
dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(config, 'borgmatic'), name, hostname='*'
|
||||
make_dump_path(borgmatic_runtime_directory), name, hostname='*'
|
||||
),
|
||||
dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*'),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(config, borgmatic_source_directory), name, hostname='*'
|
||||
make_dump_path(borgmatic_source_directory), name, hostname='*'
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def restore_data_source_dump(
|
||||
hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params
|
||||
hook_config,
|
||||
config,
|
||||
log_prefix,
|
||||
data_source,
|
||||
dry_run,
|
||||
extract_process,
|
||||
connection_params,
|
||||
borgmatic_runtime_directory,
|
||||
):
|
||||
'''
|
||||
Restore a database from the given extract stream. The database is supplied as a data source
|
||||
configuration dict, but the given hook configuration is ignored. The given configuration dict is
|
||||
used to construct the destination path, and the given log prefix is used for any log entries. If
|
||||
this is a dry run, then don't actually restore anything. Trigger the given active extract
|
||||
process (an instance of subprocess.Popen) to produce output to consume.
|
||||
configuration dict, but the given hook configuration is ignored. The given borgmatic runtime
|
||||
directory is used to construct the destination path (used for the directory format), and the
|
||||
given log prefix is used for any log entries. If this is a dry run, then don't actually restore
|
||||
anything. Trigger the given active extract process (an instance of subprocess.Popen) to produce
|
||||
output to consume.
|
||||
|
||||
If the extract process is None, then restore the dump from the filesystem rather than from an
|
||||
extract stream.
|
||||
@@ -267,7 +289,9 @@ def restore_data_source_dump(
|
||||
|
||||
all_databases = bool(data_source['name'] == 'all')
|
||||
dump_filename = dump.make_data_source_dump_filename(
|
||||
make_dump_path(config), data_source['name'], data_source.get('hostname')
|
||||
make_dump_path(borgmatic_runtime_directory),
|
||||
data_source['name'],
|
||||
data_source.get('hostname'),
|
||||
)
|
||||
psql_command = tuple(
|
||||
shlex.quote(part) for part in shlex.split(data_source.get('psql_command') or 'psql')
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
EMERGENCY_PRIORITY = 2
|
||||
|
||||
|
||||
def initialize_monitor(
|
||||
ping_url, config, config_filename, monitoring_log_level, dry_run
|
||||
): # pragma: no cover
|
||||
'''
|
||||
No initialization is necessary for this monitor.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
|
||||
'''
|
||||
Post a message to the configured Pushover application.
|
||||
If this is a dry run, then don't actually update anything.
|
||||
'''
|
||||
|
||||
run_states = hook_config.get('states', ['fail'])
|
||||
|
||||
if state.name.lower() not in run_states:
|
||||
return
|
||||
|
||||
dry_run_label = ' (dry run; not actually updating)' if dry_run else ''
|
||||
|
||||
state_config = hook_config.get(state.name.lower(), {})
|
||||
|
||||
token = hook_config.get('token')
|
||||
user = hook_config.get('user')
|
||||
|
||||
logger.info(f'{config_filename}: Updating Pushover{dry_run_label}')
|
||||
|
||||
if state_config.get('priority') == EMERGENCY_PRIORITY:
|
||||
if 'expire' not in state_config:
|
||||
logger.info(f'{config_filename}: Setting expire to default (10 min).')
|
||||
state_config['expire'] = 600
|
||||
if 'retry' not in state_config:
|
||||
logger.info(f'{config_filename}: Setting retry to default (30 sec).')
|
||||
state_config['retry'] = 30
|
||||
else:
|
||||
if 'expire' in state_config or 'retry' in state_config:
|
||||
raise ValueError(
|
||||
'The configuration parameters retry and expire should not be set when priority is not equal to 2. Please remove them from the configuration.'
|
||||
)
|
||||
|
||||
state_config = {
|
||||
key: (int(value) if key == 'html' else value) for key, value in state_config.items()
|
||||
}
|
||||
|
||||
data = dict(
|
||||
{
|
||||
'token': token,
|
||||
'user': user,
|
||||
# Default to state name. Can be overwritten by state_config below.
|
||||
'message': state.name.lower(),
|
||||
},
|
||||
**state_config,
|
||||
)
|
||||
|
||||
if not dry_run:
|
||||
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||
try:
|
||||
response = requests.post(
|
||||
'https://api.pushover.net/1/messages.json',
|
||||
headers={'Content-type': 'application/x-www-form-urlencoded'},
|
||||
data=data,
|
||||
)
|
||||
if not response.ok:
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.RequestException as error:
|
||||
logger.warning(f'{config_filename}: Pushover error: {error}')
|
||||
|
||||
|
||||
def destroy_monitor(
|
||||
ping_url_or_uuid, config, config_filename, monitoring_log_level, dry_run
|
||||
): # pragma: no cover
|
||||
'''
|
||||
No destruction is necessary for this monitor.
|
||||
'''
|
||||
pass
|
||||
+48
-28
@@ -9,15 +9,11 @@ from borgmatic.hooks import dump
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def make_dump_path(config, base_directory=None): # pragma: no cover
|
||||
def make_dump_path(base_directory): # pragma: no cover
|
||||
'''
|
||||
Given a configuration dict and an optional base directory, make the corresponding dump path. If
|
||||
a base directory isn't provided, use the borgmatic runtime directory.
|
||||
Given a base directory, make the corresponding dump path.
|
||||
'''
|
||||
return dump.make_data_source_dump_path(
|
||||
base_directory or borgmatic.config.paths.get_borgmatic_runtime_directory(config),
|
||||
'sqlite_databases',
|
||||
)
|
||||
return dump.make_data_source_dump_path(base_directory, 'sqlite_databases')
|
||||
|
||||
|
||||
def use_streaming(databases, config, log_prefix):
|
||||
@@ -28,14 +24,23 @@ def use_streaming(databases, config, log_prefix):
|
||||
return any(databases)
|
||||
|
||||
|
||||
def dump_data_sources(databases, config, log_prefix, dry_run):
|
||||
def dump_data_sources(
|
||||
databases,
|
||||
config,
|
||||
log_prefix,
|
||||
config_paths,
|
||||
borgmatic_runtime_directory,
|
||||
source_directories,
|
||||
dry_run,
|
||||
):
|
||||
'''
|
||||
Dump the given SQLite databases to a named pipe. The databases are supplied as a sequence of
|
||||
configuration dicts, as per the configuration schema. Use the given configuration dict to
|
||||
construct the destination path and the given log prefix in any log entries.
|
||||
configuration dicts, as per the configuration schema. Use the given borgmatic runtime directory
|
||||
to construct the destination path and the given log prefix in any log entries.
|
||||
|
||||
Return a sequence of subprocess.Popen instances for the dump processes ready to spew to a named
|
||||
pipe. But if this is a dry run, then don't actually dump anything and return an empty sequence.
|
||||
Also append the given source directories with the parent directory of the database dumps.
|
||||
'''
|
||||
dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
|
||||
processes = []
|
||||
@@ -52,7 +57,7 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
|
||||
f'{log_prefix}: No SQLite database at {database_path}; an empty database will be created and dumped'
|
||||
)
|
||||
|
||||
dump_path = make_dump_path(config)
|
||||
dump_path = make_dump_path(borgmatic_runtime_directory)
|
||||
dump_filename = dump.make_data_source_dump_filename(dump_path, database['name'])
|
||||
|
||||
if os.path.exists(dump_filename):
|
||||
@@ -77,46 +82,61 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
|
||||
dump.create_named_pipe_for_dump(dump_filename)
|
||||
processes.append(execute_command(command, shell=True, run_to_completion=False))
|
||||
|
||||
if not dry_run:
|
||||
source_directories.append(os.path.join(borgmatic_runtime_directory, 'sqlite_databases'))
|
||||
|
||||
return processes
|
||||
|
||||
|
||||
def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma: no cover
|
||||
def remove_data_source_dumps(
|
||||
databases, config, log_prefix, borgmatic_runtime_directory, dry_run
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Remove the given SQLite database dumps from the filesystem. The databases are supplied as a
|
||||
sequence of configuration dicts, as per the configuration schema. Use the given configuration
|
||||
dict to construct the destination path and the given log prefix in any log entries. If this is a
|
||||
dry run, then don't actually remove anything.
|
||||
Remove all database dump files for this hook regardless of the given databases. Use the
|
||||
borgmatic runtime directory to construct the destination path and the log prefix in any log
|
||||
entries. If this is a dry run, then don't actually remove anything.
|
||||
'''
|
||||
dump.remove_data_source_dumps(make_dump_path(config), 'SQLite', log_prefix, dry_run)
|
||||
dump.remove_data_source_dumps(
|
||||
make_dump_path(borgmatic_runtime_directory), 'SQLite', log_prefix, dry_run
|
||||
)
|
||||
|
||||
|
||||
def make_data_source_dump_patterns(databases, config, log_prefix, name=None): # pragma: no cover
|
||||
def make_data_source_dump_patterns(
|
||||
databases, config, log_prefix, borgmatic_runtime_directory, name=None
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Make a pattern that matches the given SQLite databases. The databases are supplied as a sequence
|
||||
of configuration dicts, as per the configuration schema.
|
||||
Given a sequence of configurations dicts, a configuration dict, a prefix to log with, the
|
||||
borgmatic runtime directory, and a database name to match, return the corresponding glob
|
||||
patterns to match the database dump in an archive.
|
||||
'''
|
||||
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
||||
|
||||
return (
|
||||
dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(config, 'borgmatic'), name, hostname='*'
|
||||
make_dump_path(borgmatic_runtime_directory), name, hostname='*'
|
||||
),
|
||||
dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*'),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(config, borgmatic_source_directory), name, hostname='*'
|
||||
make_dump_path(borgmatic_source_directory), name, hostname='*'
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def restore_data_source_dump(
|
||||
hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params
|
||||
hook_config,
|
||||
config,
|
||||
log_prefix,
|
||||
data_source,
|
||||
dry_run,
|
||||
extract_process,
|
||||
connection_params,
|
||||
borgmatic_runtime_directory,
|
||||
):
|
||||
'''
|
||||
Restore a database from the given extract stream. The database is supplied as a data source
|
||||
configuration dict, but the given hook configuration is ignored. The given configuration dict is
|
||||
used to construct the destination path, and the given log prefix is used for any log entries. If
|
||||
this is a dry run, then don't actually restore anything. Trigger the given active extract
|
||||
process (an instance of subprocess.Popen) to produce output to consume.
|
||||
configuration dict, but the given hook configuration is ignored. The given log prefix is used
|
||||
for any log entries. If this is a dry run, then don't actually restore anything. Trigger the
|
||||
given active extract process (an instance of subprocess.Popen) to produce output to consume.
|
||||
'''
|
||||
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
|
||||
database_path = connection_params['restore_path'] or data_source.get(
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
import borgmatic.config.paths
|
||||
import borgmatic.execute
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def use_streaming(hook_config, config, log_prefix): # pragma: no cover
|
||||
'''
|
||||
Return whether dump streaming is used for this hook. (Spoiler: It isn't.)
|
||||
'''
|
||||
return False
|
||||
|
||||
|
||||
BORGMATIC_SNAPSHOT_PREFIX = 'borgmatic-'
|
||||
BORGMATIC_USER_PROPERTY = 'org.torsion.borgmatic:backup'
|
||||
|
||||
|
||||
def get_datasets_to_backup(zfs_command, source_directories):
|
||||
'''
|
||||
Given a ZFS command to run and a sequence of configured source directories, find the
|
||||
intersection between the current ZFS dataset mount points and the configured borgmatic source
|
||||
directories. The idea is that these are the requested datasets to snapshot. But also include any
|
||||
datasets tagged with a borgmatic-specific user property, whether or not they appear in source
|
||||
directories.
|
||||
|
||||
Return the result as a sequence of (dataset name, mount point) pairs.
|
||||
'''
|
||||
list_output = borgmatic.execute.execute_command_and_capture_output(
|
||||
(
|
||||
zfs_command,
|
||||
'list',
|
||||
'-H',
|
||||
'-t',
|
||||
'filesystem',
|
||||
'-o',
|
||||
f'name,mountpoint,{BORGMATIC_USER_PROPERTY}',
|
||||
)
|
||||
)
|
||||
source_directories_set = set(source_directories)
|
||||
|
||||
try:
|
||||
return tuple(
|
||||
(dataset_name, mount_point)
|
||||
for line in list_output.splitlines()
|
||||
for (dataset_name, mount_point, user_property_value) in (line.rstrip().split('\t'),)
|
||||
if mount_point in source_directories_set or user_property_value == 'auto'
|
||||
)
|
||||
except ValueError:
|
||||
raise ValueError('Invalid {zfs_command} list output')
|
||||
|
||||
|
||||
def get_all_datasets(zfs_command):
|
||||
'''
|
||||
Given a ZFS command to run, return all ZFS datasets as a sequence of (dataset name, mount point)
|
||||
pairs.
|
||||
'''
|
||||
list_output = borgmatic.execute.execute_command_and_capture_output(
|
||||
(
|
||||
zfs_command,
|
||||
'list',
|
||||
'-H',
|
||||
'-t',
|
||||
'filesystem',
|
||||
'-o',
|
||||
'name,mountpoint',
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
return tuple(
|
||||
(dataset_name, mount_point)
|
||||
for line in list_output.splitlines()
|
||||
for (dataset_name, mount_point) in (line.rstrip().split('\t'),)
|
||||
)
|
||||
except ValueError:
|
||||
raise ValueError('Invalid {zfs_command} list output')
|
||||
|
||||
|
||||
def snapshot_dataset(zfs_command, full_snapshot_name): # pragma: no cover
|
||||
'''
|
||||
Given a ZFS command to run and a snapshot name of the form "dataset@snapshot", create a new ZFS
|
||||
snapshot.
|
||||
'''
|
||||
borgmatic.execute.execute_command(
|
||||
(
|
||||
zfs_command,
|
||||
'snapshot',
|
||||
full_snapshot_name,
|
||||
),
|
||||
output_log_level=logging.DEBUG,
|
||||
)
|
||||
|
||||
|
||||
def mount_snapshot(mount_command, full_snapshot_name, snapshot_mount_path): # pragma: no cover
|
||||
'''
|
||||
Given a mount command to run, an existing snapshot name of the form "dataset@snapshot", and the
|
||||
path where the snapshot should be mounted, mount the snapshot (making any necessary directories
|
||||
first).
|
||||
'''
|
||||
os.makedirs(snapshot_mount_path, mode=0o700, exist_ok=True)
|
||||
borgmatic.execute.execute_command(
|
||||
(
|
||||
mount_command,
|
||||
'-t',
|
||||
'zfs',
|
||||
full_snapshot_name,
|
||||
snapshot_mount_path,
|
||||
),
|
||||
output_log_level=logging.DEBUG,
|
||||
)
|
||||
|
||||
|
||||
def dump_data_sources(
|
||||
hook_config,
|
||||
config,
|
||||
log_prefix,
|
||||
config_paths,
|
||||
borgmatic_runtime_directory,
|
||||
source_directories,
|
||||
dry_run,
|
||||
):
|
||||
'''
|
||||
Given a ZFS configuration dict, a configuration dict, a log prefix, the borgmatic configuration
|
||||
file paths, the borgmatic runtime directory, the configured source directories, and whether this
|
||||
is a dry run, auto-detect and snapshot any ZFS dataset mount points listed in the given source
|
||||
directories and any dataset with a borgmatic-specific user property. Also update those source
|
||||
directories, replacing dataset mount points with corresponding snapshot directories so they get
|
||||
stored in the Borg archive instead of the dataset mount points. Use the log prefix in any log
|
||||
entries.
|
||||
|
||||
Return an empty sequence, since there are no ongoing dump processes from this hook.
|
||||
|
||||
If this is a dry run, then don't actually snapshot anything.
|
||||
'''
|
||||
dry_run_label = ' (dry run; not actually snapshotting anything)' if dry_run else ''
|
||||
logger.info(f'{log_prefix}: Snapshotting ZFS datasets{dry_run_label}')
|
||||
|
||||
# List ZFS datasets to get their mount points.
|
||||
zfs_command = hook_config.get('zfs_command', 'zfs')
|
||||
requested_datasets = get_datasets_to_backup(zfs_command, source_directories)
|
||||
|
||||
# Snapshot each dataset, rewriting source directories to use the snapshot paths.
|
||||
snapshot_name = f'{BORGMATIC_SNAPSHOT_PREFIX}{os.getpid()}'
|
||||
|
||||
for dataset_name, mount_point in requested_datasets:
|
||||
full_snapshot_name = f'{dataset_name}@{snapshot_name}'
|
||||
logger.debug(f'{log_prefix}: Creating ZFS snapshot {full_snapshot_name}{dry_run_label}')
|
||||
|
||||
if not dry_run:
|
||||
snapshot_dataset(zfs_command, full_snapshot_name)
|
||||
|
||||
# Mount the snapshot into a particular named temporary directory so that the snapshot ends
|
||||
# up in the Borg archive at the "original" dataset mount point path.
|
||||
snapshot_mount_path_for_borg = os.path.join(
|
||||
os.path.normpath(borgmatic_runtime_directory),
|
||||
'zfs_snapshots',
|
||||
'.', # Borg 1.4+ "slashdot" hack.
|
||||
mount_point.lstrip(os.path.sep),
|
||||
)
|
||||
snapshot_mount_path = os.path.normpath(snapshot_mount_path_for_borg)
|
||||
logger.debug(
|
||||
f'{log_prefix}: Mounting ZFS snapshot {full_snapshot_name} at {snapshot_mount_path}{dry_run_label}'
|
||||
)
|
||||
|
||||
if not dry_run:
|
||||
mount_snapshot(
|
||||
hook_config.get('mount_command', 'mount'), full_snapshot_name, snapshot_mount_path
|
||||
)
|
||||
|
||||
if mount_point in source_directories:
|
||||
source_directories.remove(mount_point)
|
||||
|
||||
source_directories.append(snapshot_mount_path_for_borg)
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def unmount_snapshot(umount_command, snapshot_mount_path): # pragma: no cover
|
||||
'''
|
||||
Given a umount command to run and the mount path of a snapshot, unmount it.
|
||||
'''
|
||||
borgmatic.execute.execute_command(
|
||||
(
|
||||
umount_command,
|
||||
snapshot_mount_path,
|
||||
),
|
||||
output_log_level=logging.DEBUG,
|
||||
)
|
||||
|
||||
|
||||
def destroy_snapshot(zfs_command, full_snapshot_name): # pragma: no cover
|
||||
'''
|
||||
Given a ZFS command to run and the name of a snapshot in the form "dataset@snapshot", destroy
|
||||
it.
|
||||
'''
|
||||
borgmatic.execute.execute_command(
|
||||
(
|
||||
zfs_command,
|
||||
'destroy',
|
||||
full_snapshot_name,
|
||||
),
|
||||
output_log_level=logging.DEBUG,
|
||||
)
|
||||
|
||||
|
||||
def get_all_snapshots(zfs_command):
|
||||
'''
|
||||
Given a ZFS command to run, return all ZFS snapshots as a sequence of full snapshot names of the
|
||||
form "dataset@snapshot".
|
||||
'''
|
||||
list_output = borgmatic.execute.execute_command_and_capture_output(
|
||||
(
|
||||
zfs_command,
|
||||
'list',
|
||||
'-H',
|
||||
'-t',
|
||||
'snapshot',
|
||||
'-o',
|
||||
'name',
|
||||
)
|
||||
)
|
||||
|
||||
return tuple(line.rstrip() for line in list_output.splitlines())
|
||||
|
||||
|
||||
def remove_data_source_dumps(hook_config, config, log_prefix, borgmatic_runtime_directory, dry_run):
|
||||
'''
|
||||
Given a ZFS configuration dict, a configuration dict, a log prefix, the borgmatic runtime
|
||||
directory, and whether this is a dry run, unmount and destroy any ZFS snapshots created by
|
||||
borgmatic. Use the log prefix in any log entries. If this is a dry run, then don't actually
|
||||
remove anything.
|
||||
'''
|
||||
dry_run_label = ' (dry run; not actually removing anything)' if dry_run else ''
|
||||
|
||||
# Unmount snapshots.
|
||||
zfs_command = hook_config.get('zfs_command', 'zfs')
|
||||
|
||||
try:
|
||||
datasets = get_all_datasets(zfs_command)
|
||||
except FileNotFoundError:
|
||||
logger.debug(f'{log_prefix}: Could not find "{zfs_command}" command')
|
||||
return
|
||||
except subprocess.CalledProcessError as error:
|
||||
logger.debug(f'{log_prefix}: {error}')
|
||||
return
|
||||
|
||||
snapshots_glob = os.path.join(
|
||||
borgmatic.config.paths.replace_temporary_subdirectory_with_glob(
|
||||
os.path.normpath(borgmatic_runtime_directory),
|
||||
),
|
||||
'zfs_snapshots',
|
||||
)
|
||||
logger.debug(
|
||||
f'{log_prefix}: Looking for snapshots to remove in {snapshots_glob}{dry_run_label}'
|
||||
)
|
||||
umount_command = hook_config.get('umount_command', 'umount')
|
||||
|
||||
for snapshots_directory in glob.glob(snapshots_glob):
|
||||
if not os.path.isdir(snapshots_directory):
|
||||
continue
|
||||
|
||||
# This might fail if the directory is already mounted, but we swallow errors here since
|
||||
# we'll try again below. The point of doing it here is that we don't want to try to unmount
|
||||
# a non-mounted directory (which *will* fail), and probing for whether a directory is
|
||||
# mounted is tough to do in a cross-platform way.
|
||||
if not dry_run:
|
||||
shutil.rmtree(snapshots_directory, ignore_errors=True)
|
||||
|
||||
for _, mount_point in datasets:
|
||||
snapshot_mount_path = os.path.join(snapshots_directory, mount_point.lstrip(os.path.sep))
|
||||
if not os.path.isdir(snapshot_mount_path):
|
||||
continue
|
||||
|
||||
logger.debug(
|
||||
f'{log_prefix}: Unmounting ZFS snapshot at {snapshot_mount_path}{dry_run_label}'
|
||||
)
|
||||
|
||||
if not dry_run:
|
||||
try:
|
||||
unmount_snapshot(umount_command, snapshot_mount_path)
|
||||
except FileNotFoundError:
|
||||
logger.debug(f'{log_prefix}: Could not find "{umount_command}" command')
|
||||
return
|
||||
except subprocess.CalledProcessError as error:
|
||||
logger.debug(f'{log_prefix}: {error}')
|
||||
return
|
||||
|
||||
if not dry_run:
|
||||
shutil.rmtree(snapshots_directory)
|
||||
|
||||
# Destroy snapshots.
|
||||
full_snapshot_names = get_all_snapshots(zfs_command)
|
||||
|
||||
for full_snapshot_name in full_snapshot_names:
|
||||
# Only destroy snapshots that borgmatic actually created!
|
||||
if not full_snapshot_name.split('@')[-1].startswith(BORGMATIC_SNAPSHOT_PREFIX):
|
||||
continue
|
||||
|
||||
logger.debug(f'{log_prefix}: Destroying ZFS snapshot {full_snapshot_name}{dry_run_label}')
|
||||
|
||||
if not dry_run:
|
||||
destroy_snapshot(zfs_command, full_snapshot_name)
|
||||
|
||||
|
||||
def make_data_source_dump_patterns(
|
||||
hook_config, config, log_prefix, borgmatic_runtime_directory, name=None
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Restores aren't implemented, because stored files can be extracted directly with "extract".
|
||||
'''
|
||||
return ()
|
||||
|
||||
|
||||
def restore_data_source_dump(
|
||||
hook_config,
|
||||
config,
|
||||
log_prefix,
|
||||
data_source,
|
||||
dry_run,
|
||||
extract_process,
|
||||
connection_params,
|
||||
borgmatic_runtime_directory,
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Restores aren't implemented, because stored files can be extracted directly with "extract".
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
+1
-1
@@ -2,7 +2,7 @@ FROM docker.io/alpine:3.20.1 AS borgmatic
|
||||
|
||||
COPY . /app
|
||||
RUN apk add --no-cache py3-pip py3-ruamel.yaml py3-ruamel.yaml.clib
|
||||
RUN pip install --break-system-packages --no-cache /app && generate-borgmatic-config && chmod +r /etc/borgmatic/config.yaml
|
||||
RUN pip install --break-system-packages --no-cache /app && borgmatic config generate && chmod +r /etc/borgmatic/config.yaml
|
||||
RUN borgmatic --help > /command-line.txt \
|
||||
&& for action in repo-create transfer create prune compact check delete extract config "config bootstrap" "config generate" "config validate" export-tar mount umount repo-delete restore repo-list list repo-info info break-lock "key export" "key change-passphrase" borg; do \
|
||||
echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \
|
||||
|
||||
@@ -3,7 +3,7 @@ title: How to add preparation and cleanup steps to backups
|
||||
eleventyNavigation:
|
||||
key: 🧹 Add preparation and cleanup steps
|
||||
parent: How-to guides
|
||||
order: 9
|
||||
order: 10
|
||||
---
|
||||
## Preparation and cleanup hooks
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ title: How to backup to a removable drive or an intermittent server
|
||||
eleventyNavigation:
|
||||
key: 💾 Backup to a removable drive/server
|
||||
parent: How-to guides
|
||||
order: 10
|
||||
order: 11
|
||||
---
|
||||
## Occasional backups
|
||||
|
||||
|
||||
@@ -63,23 +63,9 @@ dump formats, which can't stream and therefore do consume temporary disk
|
||||
space. Additionally, prior to borgmatic 1.5.3, all database dumps consumed
|
||||
temporary disk space.)
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.9.0</span> To support
|
||||
this, borgmatic creates temporary streaming database dumps within
|
||||
`/run/user/$UID/borgmatic` by default (where `$UID` is the current user's ID).
|
||||
To customize the `/run/user/$UID` portion of this path, set the
|
||||
`user_runtime_directory` option in borgmatic's configuration. Alternatively,
|
||||
set the `XDG_RUNTIME_DIR` environment variable (often already set to
|
||||
`/run/user/$UID`).
|
||||
|
||||
<span class="minilink minilink-addedin">Prior to version 1.9.0</span>
|
||||
borgmatic created temporary streaming database dumps within the `~/.borgmatic`
|
||||
directory by default. At that time, the path was configurable by the
|
||||
`borgmatic_source_directory` configuration option (now deprecated).
|
||||
|
||||
Also note that using a database hook implicitly enables the
|
||||
`read_special` configuration option (even if it's disabled in your
|
||||
configuration) to support this dump and restore streaming. See Limitations
|
||||
below for more on this.
|
||||
Also note that using a database hook implicitly enables the `read_special`
|
||||
configuration option (even if it's disabled in your configuration) to support
|
||||
this dump and restore streaming. See Limitations below for more on this.
|
||||
|
||||
Here's a more involved example that connects to remote databases:
|
||||
|
||||
@@ -127,6 +113,40 @@ additional customization of the options passed to database commands (when
|
||||
listing databases, restoring databases, etc.).
|
||||
|
||||
|
||||
### Runtime directory
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.9.0</span> To support
|
||||
streaming database dumps to Borg, borgmatic uses a runtime directory for
|
||||
temporary file storage, probing the following locations (in order) to find it:
|
||||
|
||||
1. The `user_runtime_directory` borgmatic configuration option.
|
||||
2. The `XDG_RUNTIME_DIR` environment variable, usually `/run/user/$UID`
|
||||
(where `$UID` is the current user's ID), automatically set by PAM on Linux
|
||||
for a user with a session.
|
||||
3. <span class="minilink minilink-addedin">New in version 1.9.2</span>The
|
||||
`RUNTIME_DIRECTORY` environment variable, set by systemd if
|
||||
`RuntimeDirectory=borgmatic` is added to borgmatic's systemd service file.
|
||||
4. <span class="minilink minilink-addedin">New in version 1.9.1</span>The
|
||||
`TMPDIR` environment variable, set on macOS for a user with a session,
|
||||
among other operating systems.
|
||||
5. <span class="minilink minilink-addedin">New in version 1.9.1</span>The
|
||||
`TEMP` environment variable, set on various systems.
|
||||
6. <span class="minilink minilink-addedin">New in version 1.9.2</span>
|
||||
Hard-coded `/tmp`. <span class="minilink minilink-addedin">Prior to
|
||||
version 1.9.2</span>This was instead hard-coded to `/run/user/$UID`.
|
||||
|
||||
Regardless of the runtime directory selected, borgmatic stores its files
|
||||
within a `borgmatic` subdirectory of the runtime directory. Additionally, in
|
||||
the case of `TMPDIR`, `TEMP`, and the hard-coded `/tmp`, borgmatic creates a
|
||||
randomly named subdirectory in an effort to reduce path collisions in shared
|
||||
system temporary directories.
|
||||
|
||||
<span class="minilink minilink-addedin">Prior to version 1.9.0</span>
|
||||
borgmatic created temporary streaming database dumps within the `~/.borgmatic`
|
||||
directory by default. At that time, the path was configurable by the
|
||||
`borgmatic_source_directory` configuration option (now deprecated).
|
||||
|
||||
|
||||
### All databases
|
||||
|
||||
If you want to dump all databases on a host, use `all` for the database name:
|
||||
|
||||
@@ -3,7 +3,7 @@ title: How to customize warnings and errors
|
||||
eleventyNavigation:
|
||||
key: 💥 Customize warnings/errors
|
||||
parent: How-to guides
|
||||
order: 12
|
||||
order: 13
|
||||
---
|
||||
## When things go wrong
|
||||
|
||||
|
||||
@@ -62,8 +62,8 @@ actions](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#skipping-acti
|
||||
Another option is to customize your consistency checks. By default, if you
|
||||
omit consistency checks from configuration, borgmatic runs full-repository
|
||||
checks (`repository`) and per-archive checks (`archives`) within each
|
||||
repository. (Although see below about check frequency.) This is equivalent to
|
||||
what `borg check` does if run without options.
|
||||
repository, running the checks on a monthly basis. (See below about setting
|
||||
your own check frequency.)
|
||||
|
||||
But if you find that archive checks are too slow, for example, you can
|
||||
configure borgmatic to run repository checks only. Configure this in the
|
||||
@@ -87,13 +87,16 @@ checks:
|
||||
```
|
||||
|
||||
|
||||
Here are the available checks from fastest to slowest:
|
||||
Here are the available checks, roughly from fastest to slowest:
|
||||
|
||||
* `repository`: Checks the consistency of the repository itself.
|
||||
* `archives`: Checks all of the archives in the repository.
|
||||
* `archives`: Checks all of the archives' metadata in the repository.
|
||||
* `repository`: Checks the consistency of the whole repository. The checks run
|
||||
on the server and do not cause significant network traffic.
|
||||
* `extract`: Performs an extraction dry-run of the latest archive.
|
||||
* `data`: Verifies the data integrity of all archives contents, decrypting and decompressing all data.
|
||||
* `spot`: Compares file counts and contents between your source files and the latest archive.
|
||||
* `data`: Verifies the data integrity of all archives contents, decrypting
|
||||
and decompressing all data.
|
||||
* `spot`: Compares file counts and contents between your source files and the
|
||||
latest archive.
|
||||
|
||||
Note that the `data` check is a more thorough version of the `archives` check,
|
||||
so enabling the `data` check implicitly enables the `archives` check as well.
|
||||
@@ -113,11 +116,11 @@ file). Because if something goes wrong with your source files, most
|
||||
consistency checks will still pass with flying colors and you won't discover
|
||||
there's a problem until you go to restore.
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.8.10</span> <span
|
||||
class="minilink minilink-addedin">Beta feature</span> That's where the spot
|
||||
check comes in. This check actually compares your source file counts and data
|
||||
against those in the latest archive, potentially catching problems like
|
||||
incorrect excludes, inadvertent deletes, files changed by malware, etc.
|
||||
<span class="minilink minilink-addedin">New in version 1.8.10</span> That's
|
||||
where the spot check comes in. This check actually compares your source file
|
||||
counts and data against those in the latest archive, potentially catching
|
||||
problems like incorrect excludes, inadvertent deletes, files changed by
|
||||
malware, etc.
|
||||
|
||||
But because an exhaustive comparison of all source files against the latest
|
||||
archive might be too slow, the spot check supports *sampling* a percentage of
|
||||
@@ -180,11 +183,6 @@ to run it immediately after a `create` action (borgmatic's default behavior).
|
||||
Instead, it may make more sense to run the spot check on a separate schedule
|
||||
from `create`.
|
||||
|
||||
As long as the spot check feature is in beta, it may be subject to breaking
|
||||
changes. But feel free to use it in production if you're okay with that
|
||||
caveat, and please [provide any
|
||||
feedback](https://torsion.org/borgmatic/#issues) you have on this feature.
|
||||
|
||||
|
||||
### Check frequency
|
||||
|
||||
@@ -240,6 +238,11 @@ this frequency logic, borgmatic records check timestamps within the
|
||||
portion of this path, set the `user_state_directory` configuration option.
|
||||
Alternatively, set the `XDG_STATE_HOME` environment variable.
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.9.2</span>The
|
||||
`STATE_DIRECTORY` environment variable also works for this purpose. It's set
|
||||
by systemd if `StateDirectory=borgmatic` is added to borgmatic's systemd
|
||||
service file.
|
||||
|
||||
<span class="minilink minilink-addedin">Prior to version 1.9.0</span>
|
||||
borgmatic recorded check timestamps within the `~/.borgmatic` directory. At
|
||||
that time, the path was configurable by the `borgmatic_source_directory`
|
||||
|
||||
@@ -3,7 +3,7 @@ title: How to develop on borgmatic
|
||||
eleventyNavigation:
|
||||
key: 🏗️ Develop on borgmatic
|
||||
parent: How-to guides
|
||||
order: 14
|
||||
order: 15
|
||||
---
|
||||
## Source code
|
||||
|
||||
|
||||
@@ -156,12 +156,19 @@ borgmatic umount --mount-point /mnt
|
||||
|
||||
## Extract the configuration files used to create an archive
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.7.15</span> borgmatic
|
||||
automatically stores all the configuration files used to create an archive
|
||||
inside the archive itself. They are stored in the archive using their full
|
||||
paths from the machine being backed up. This is useful in cases where you've
|
||||
lost a configuration file or you want to see what configurations were used to
|
||||
create a particular archive.
|
||||
<span class="minilink minilink-addedin">New in version 1.7.15</span> As part
|
||||
of creating a backup archive, borgmatic automatically includes all of the
|
||||
configuration files used when creating it, storing them inside the archive
|
||||
itself with their full paths from the machine being backed up. This is useful
|
||||
in cases where you've lost a configuration file or you want to see what
|
||||
configurations were used to create a particular archive.
|
||||
|
||||
To support this, borgmatic creates a manifest file that records the paths of
|
||||
all the borgmatic configuration files stored within an archive. The file gets
|
||||
written to borgmatic's runtime directory on disk and then stored within the
|
||||
archive. See the [runtime directory
|
||||
documentation](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#runtime-directory)
|
||||
for how and where that happens.
|
||||
|
||||
To extract the configuration files from an archive, use the `config bootstrap`
|
||||
action. For example:
|
||||
@@ -195,13 +202,25 @@ borgmatic config bootstrap --repository repo.borg --archive host-2023-01-02T04:0
|
||||
See the output of `config bootstrap --help` for additional flags you may need
|
||||
for bootstrapping.
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.8.1</span> Set the
|
||||
`store_config_files` option to `false` to disable the automatic backup of
|
||||
borgmatic configuration files, for instance if they contain sensitive
|
||||
information you don't want to store even inside your encrypted backups. If you
|
||||
do this though, the `config bootstrap` action will no longer work.
|
||||
<span class="minilink minilink-addedin">New in version 1.9.3</span>
|
||||
If your borgmatic configuration files contain sensitive information you don't
|
||||
want to store even inside your encrypted backups, you can disable the
|
||||
automatic backup of the configuration files. To do this, set the
|
||||
`store_config_files` option under the `bootstrap` hook to `false`. For
|
||||
instance:
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.8.7</span> Included
|
||||
configuration files are stored in each backup archive. This means that the
|
||||
`config bootstrap` action not only extracts the top-level configuration files
|
||||
but also the includes they depend upon.
|
||||
```yaml
|
||||
bootstrap:
|
||||
store_config_files: false
|
||||
```
|
||||
|
||||
If you do this though, the `config bootstrap` action will no longer work.
|
||||
|
||||
<span class="minilink minilink-addedin">In version 1.8.1 through 1.9.2</span>
|
||||
The `store_config_files` option was at the global scope instead of under the
|
||||
`bootstrap` hook.
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.8.7</span>
|
||||
Configuration file includes are stored in each backup archive. This means that
|
||||
the `config bootstrap` action not only extracts the top-level configuration
|
||||
files but also the includes they depend upon.
|
||||
|
||||
@@ -119,10 +119,10 @@ archive, regardless of the user who performs the backup. (Note that Borg
|
||||
doesn't store the leading `/`.)
|
||||
|
||||
<span class="minilink minilink-addedin">With Borg version 1.2 and
|
||||
earlier</span>Database dump files are stored at `/var/run/$UID/borgmatic`
|
||||
(where `$UID` is the current user's ID) unless overridden by the
|
||||
`user_runtime_directory` configuration option or the `XDG_STATE_HOME`
|
||||
environment variable.
|
||||
earlier</span>Database dump files are stored at a path dependent on the [runtime
|
||||
directory](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#runtime-directory)
|
||||
in use at the time the archive was created, as Borg 1.2 and earlier do not
|
||||
support path rewriting.
|
||||
|
||||
<span class="minilink minilink-addedin">Prior to borgmatic version
|
||||
1.9.0</span>Database dump files were instead stored at `~/.borgmatic` within
|
||||
|
||||
@@ -46,6 +46,7 @@ them as backups happen:
|
||||
* [Healthchecks](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#healthchecks-hook)
|
||||
* [ntfy](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#ntfy-hook)
|
||||
* [PagerDuty](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook)
|
||||
* [Pushover](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pushover-hook)
|
||||
* [Uptime Kuma](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#uptime-kuma-hook)
|
||||
* [Zabbix](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#zabbix-hook)
|
||||
|
||||
@@ -290,6 +291,76 @@ If you have any issues with the integration, [please contact
|
||||
us](https://torsion.org/borgmatic/#support-and-contributing).
|
||||
|
||||
|
||||
## Pushover hook
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.9.2</span>
|
||||
[Pushover](https://pushover.net) makes it easy to get real-time notifications
|
||||
on your Android, iPhone, iPad, and Desktop (Android Wear and Apple Watch,
|
||||
too!).
|
||||
|
||||
First, create a Pushover account and login on your mobile device. Create an
|
||||
Application in your Pushover dashboard.
|
||||
|
||||
Then, configure borgmatic with your user's unique "User Key" found in your
|
||||
Pushover dashboard and the unique "API Token" from the created Application.
|
||||
|
||||
Here's a basic example:
|
||||
|
||||
|
||||
```yaml
|
||||
pushover:
|
||||
token: 7ms6TXHpTokTou2P6x4SodDeentHRa
|
||||
user: hwRwoWsXMBWwgrSecfa9EfPey55WSN
|
||||
```
|
||||
|
||||
|
||||
With this configuration, borgmatic creates a Pushover event for your service
|
||||
whenever borgmatic fails, but only when any of the `create`, `prune`, `compact`,
|
||||
or `check` actions are run. Note that borgmatic does not contact Pushover
|
||||
when a backup starts or when it ends without error by default.
|
||||
|
||||
You can configure Pushover to have custom parameters declared for borgmatic's
|
||||
`start`, `fail` and `finish` hooks states.
|
||||
|
||||
Here's a more advanced example:
|
||||
|
||||
|
||||
```yaml
|
||||
pushover:
|
||||
token: 7ms6TXHpTokTou2P6x4SodDeentHRa
|
||||
user: hwRwoWsXMBWwgrSecfa9EfPey55WSN
|
||||
start:
|
||||
message: "Backup <b>Started</b>"
|
||||
priority: -2
|
||||
title: "Backup Started"
|
||||
html: True
|
||||
ttl: 10 # Message will be deleted after 10 seconds.
|
||||
fail:
|
||||
message: "Backup <font color='#ff6961'>Failed</font>"
|
||||
priority: 2 # Requests acknowledgement for messages.
|
||||
expire: 600 # Used only for priority 2. Default is 600 seconds.
|
||||
retry: 30 # Used only for priority 2. Default is 30 seconds.
|
||||
device: "pixel8"
|
||||
title: "Backup Failed"
|
||||
html: True
|
||||
sound: "siren"
|
||||
url: "https://ticketing-system.example.com/login"
|
||||
url_title: "Login to ticketing system"
|
||||
finish:
|
||||
message: "Backup <font color='#77dd77'>Finished</font>"
|
||||
priority: 0
|
||||
title: "Backup Finished"
|
||||
html: True
|
||||
ttl: 60
|
||||
url: "https://ticketing-system.example.com/login"
|
||||
url_title: "Login to ticketing system"
|
||||
states:
|
||||
- start
|
||||
- finish
|
||||
- fail
|
||||
```
|
||||
|
||||
|
||||
## ntfy hook
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.6.3</span>
|
||||
|
||||
@@ -39,9 +39,15 @@ Save your password as an encrypted credential to `/etc/credstore.encrypted/borgm
|
||||
# systemd-ask-password -n | systemd-creds encrypt - /etc/credstore.encrypted/borgmatic.pw
|
||||
```
|
||||
|
||||
Then uncomment or use the following in your configuration file:
|
||||
|
||||
```yaml
|
||||
encryption_passcommand: "cat ${CREDENTIALS_DIRECTORY}/borgmatic.pw"
|
||||
```
|
||||
|
||||
Note that the name `borgmatic.pw` is hardcoded in the systemd service file.
|
||||
|
||||
If you use multiple different passwords, save them as encrypted credentials to `/etc/credstore.encrypted/borgmatic/`, e.g.,
|
||||
To use multiple different passwords, save them as encrypted credentials to `/etc/credstore.encrypted/borgmatic/`, e.g.,
|
||||
|
||||
```
|
||||
# mkdir /etc/credstore.encrypted/borgmatic
|
||||
@@ -49,17 +55,29 @@ If you use multiple different passwords, save them as encrypted credentials to `
|
||||
# systemd-ask-password -n | systemd-creds encrypt --name=borgmatic_pw2 - /etc/credstore.encrypted/borgmatic/pw2
|
||||
...
|
||||
```
|
||||
|
||||
Ensure that the file names, (e.g. `backupserver1`) match the corresponding part of
|
||||
the `--name` option *after* the underscore (_). The `borgmatic` folder is hardcoded in the systemd service file.
|
||||
the `--name` option *after* the underscore (_), and that the part *before*
|
||||
the underscore matches the directory name (e.g. `borgmatic`).
|
||||
|
||||
Then uncomment or use one of the following in your configuration file. Adjust `borgmatic_backupserver1`
|
||||
according to the name given to the credential.
|
||||
Then, uncomment the appropriate line in the systemd service file:
|
||||
|
||||
```yaml
|
||||
encryption_passcommand: "cat ${CREDENTIALS_DIRECTORY}/borgmatic.pw"
|
||||
```
|
||||
# systemctl edit borgmatic.service
|
||||
...
|
||||
# Load multiple encrypted credentials.
|
||||
LoadCredentialEncrypted=borgmatic:/etc/credstore.encrypted/borgmatic/
|
||||
```
|
||||
|
||||
Finally, use the following in your configuration file:
|
||||
|
||||
```
|
||||
encryption_passcommand: "cat ${CREDENTIALS_DIRECTORY}/borgmatic_backupserver1"
|
||||
```
|
||||
|
||||
Adjust `borgmatic_backupserver1` according to the name given to the credential
|
||||
and the directory set in the service file.
|
||||
|
||||
### Environment variable interpolation
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.6.4</span> borgmatic
|
||||
|
||||
@@ -3,7 +3,7 @@ title: How to run arbitrary Borg commands
|
||||
eleventyNavigation:
|
||||
key: 🔧 Run arbitrary Borg commands
|
||||
parent: How-to guides
|
||||
order: 11
|
||||
order: 12
|
||||
---
|
||||
## Running Borg with borgmatic
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: How to snapshot your filesystems
|
||||
eleventyNavigation:
|
||||
key: 📸 Snapshot your filesystems
|
||||
parent: How-to guides
|
||||
order: 9
|
||||
---
|
||||
## Filesystem hooks
|
||||
|
||||
Many filesystems support taking snapshots—point-in-time, read-only "copies" of
|
||||
your data, ideal for backing up files that may change during the backup. These
|
||||
snapshots initially don't use any additional storage space and can be made
|
||||
almost instantly.
|
||||
|
||||
To help automate backup of these filesystems, borgmatic can use them to take
|
||||
snapshots.
|
||||
|
||||
|
||||
### ZFS
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.9.3</span> <span
|
||||
class="minilink minilink-addedin">Beta feature</span> borgmatic supports
|
||||
taking snapshots with the [ZFS filesystem](https://openzfs.org/) and sending
|
||||
those snapshots to Borg for backup.
|
||||
|
||||
To use this feature, first you need one or more mounted ZFS datasets. Then,
|
||||
enable ZFS within borgmatic by adding the following line to your configuration
|
||||
file:
|
||||
|
||||
```yaml
|
||||
zfs:
|
||||
```
|
||||
|
||||
No other options are necessary to enable ZFS support, but if desired you can
|
||||
override some of the commands used by the ZFS hook. For instance:
|
||||
|
||||
```yaml
|
||||
zfs:
|
||||
zfs_command: /usr/local/bin/zfs
|
||||
mount_command: /usr/local/bin/mount
|
||||
umount_command: /usr/local/bin/umount
|
||||
```
|
||||
|
||||
As long as the ZFS hook is in beta, it may be subject to breaking changes
|
||||
and/or may not work well for your use cases. But feel free to use it in
|
||||
production if you're okay with these caveats, and please [provide any
|
||||
feedback](https://torsion.org/borgmatic/#issues) you have on this feature.
|
||||
|
||||
|
||||
#### Dataset discovery
|
||||
|
||||
You have a couple of options for borgmatic to find and backup your ZFS datasets:
|
||||
|
||||
* For any dataset you'd like backed up, add its mount point to borgmatic's
|
||||
`source_directories`.
|
||||
* Or set the borgmatic-specific user property
|
||||
`org.torsion.borgmatic:backup=auto` onto your dataset, e.g. by running `zfs
|
||||
set org.torsion.borgmatic:backup=auto datasetname`. Then borgmatic can find
|
||||
and backup these datasets.
|
||||
|
||||
If you have multiple borgmatic configuration files with ZFS enabled, and you'd
|
||||
like particular datasets to be backed up only for particular configuration
|
||||
files, use the `source_directories` option instead of the user property.
|
||||
|
||||
During a backup, borgmatic automatically snapshots these discovered datasets,
|
||||
temporary mounts the snapshots within its [runtime
|
||||
directory](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#runtime-directory),
|
||||
and includes the snapshotted files in the files sent to Borg. borgmatic is
|
||||
also responsible for cleaning up (destroying) these snapshots after a backup
|
||||
completes.
|
||||
|
||||
Additionally, borgmatic rewrites the snapshot file paths so that they appear
|
||||
at their original dataset locations in a Borg archive. For instance, if your
|
||||
dataset is mounted at `/mnt/dataset`, then the snapshotted files will appear
|
||||
in an archive at `/mnt/dataset` as well.
|
||||
|
||||
<span class="minilink minilink-addedin">With Borg version 1.2 and
|
||||
earlier</span>Snapshotted files are instead stored at a path dependent on the
|
||||
[runtime
|
||||
directory](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#runtime-directory)
|
||||
in use at the time the archive was created, as Borg 1.2 and earlier do not
|
||||
support path rewriting.
|
||||
|
||||
|
||||
#### Extract a dataset
|
||||
|
||||
Filesystem snapshots are stored in a Borg archive as normal files, so
|
||||
you can use the standard
|
||||
[extract action](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/) to
|
||||
extract them.
|
||||
@@ -3,7 +3,7 @@ title: How to upgrade borgmatic and Borg
|
||||
eleventyNavigation:
|
||||
key: 📦 Upgrade borgmatic/Borg
|
||||
parent: How-to guides
|
||||
order: 13
|
||||
order: 14
|
||||
---
|
||||
## Upgrading borgmatic
|
||||
|
||||
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
+3
-4
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "borgmatic"
|
||||
version = "1.9.0"
|
||||
version = "1.9.3"
|
||||
authors = [
|
||||
{ name="Dan Helfman", email="witten@torsion.org" },
|
||||
]
|
||||
@@ -37,8 +37,7 @@ Homepage = "https://torsion.org/borgmatic"
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0"]
|
||||
# allow looking for conf in setup.{cfg,py}
|
||||
#build-backend = "setuptools.build_meta"
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["borgmatic*"]
|
||||
@@ -50,7 +49,7 @@ skip-string-normalization = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = "tests"
|
||||
addopts = "--cov-report term-missing:skip-covered --cov=borgmatic --ignore=tests/end-to-end"
|
||||
addopts = "--cov-report term-missing:skip-covered --cov=borgmatic --no-cov-on-fail --cov-fail-under=100 --ignore=tests/end-to-end"
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
|
||||
@@ -9,11 +9,15 @@ Documentation=https://torsion.org/borgmatic/
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RuntimeDirectory=borgmatic
|
||||
StateDirectory=borgmatic
|
||||
|
||||
# Load encrypted credentials.
|
||||
LoadCredentialEncrypted=borgmatic:/etc/credstore.encrypted/borgmatic/
|
||||
# Load single encrypted credential.
|
||||
LoadCredentialEncrypted=borgmatic.pw
|
||||
|
||||
# Load multiple encrypted credentials.
|
||||
# LoadCredentialEncrypted=borgmatic:/etc/credstore.encrypted/borgmatic/
|
||||
|
||||
# Security settings for systemd running as root, optional but recommended to improve security. You
|
||||
# can disable individual settings if they cause problems for your use case. For more details, see
|
||||
# the systemd manual: https://www.freedesktop.org/software/systemd/man/systemd.exec.html
|
||||
|
||||
@@ -8,7 +8,7 @@ MAXIMUM_LINE_LENGTH = 80
|
||||
|
||||
|
||||
def test_schema_line_length_stays_under_limit():
|
||||
schema_file = open('borgmatic/config/schema.yaml')
|
||||
schema_file = open(borgmatic.config.validate.schema_filename())
|
||||
|
||||
for line in schema_file.readlines():
|
||||
assert len(line.rstrip('\n')) <= MAXIMUM_LINE_LENGTH
|
||||
|
||||
@@ -67,6 +67,7 @@ def test_parse_configuration_transforms_file_into_mapping():
|
||||
'keep_hourly': 24,
|
||||
'keep_minutely': 60,
|
||||
'checks': [{'name': 'repository'}, {'name': 'archives'}],
|
||||
'bootstrap': {},
|
||||
}
|
||||
assert config_paths == {'/tmp/config.yaml'}
|
||||
assert logs == []
|
||||
@@ -90,6 +91,7 @@ def test_parse_configuration_passes_through_quoted_punctuation():
|
||||
assert config == {
|
||||
'source_directories': [f'/home/{string.punctuation}'],
|
||||
'repositories': [{'path': 'test.borg'}],
|
||||
'bootstrap': {},
|
||||
}
|
||||
assert config_paths == {'/tmp/config.yaml'}
|
||||
assert logs == []
|
||||
@@ -150,6 +152,7 @@ def test_parse_configuration_inlines_include_inside_deprecated_section():
|
||||
'repositories': [{'path': 'hostname.borg'}],
|
||||
'keep_daily': 7,
|
||||
'keep_hourly': 24,
|
||||
'bootstrap': {},
|
||||
}
|
||||
assert config_paths == {'/tmp/include.yaml', '/tmp/config.yaml'}
|
||||
assert len(logs) == 1
|
||||
@@ -185,6 +188,7 @@ def test_parse_configuration_merges_include():
|
||||
'repositories': [{'path': 'hostname.borg'}],
|
||||
'keep_daily': 1,
|
||||
'keep_hourly': 24,
|
||||
'bootstrap': {},
|
||||
}
|
||||
assert config_paths == {'/tmp/include.yaml', '/tmp/config.yaml'}
|
||||
assert logs == []
|
||||
@@ -248,6 +252,7 @@ def test_parse_configuration_applies_overrides():
|
||||
'source_directories': ['/home'],
|
||||
'repositories': [{'path': 'hostname.borg'}],
|
||||
'local_path': 'borg2',
|
||||
'bootstrap': {},
|
||||
}
|
||||
assert config_paths == {'/tmp/config.yaml'}
|
||||
assert logs == []
|
||||
@@ -274,6 +279,7 @@ def test_parse_configuration_applies_normalization_after_environment_variable_in
|
||||
'source_directories': ['/home'],
|
||||
'repositories': [{'path': 'ssh://user@hostname/./repo'}],
|
||||
'exclude_if_present': ['.nobackup'],
|
||||
'bootstrap': {},
|
||||
}
|
||||
assert config_paths == {'/tmp/config.yaml'}
|
||||
assert logs
|
||||
|
||||
@@ -16,9 +16,6 @@ def test_get_config_paths_returns_list_of_config_paths():
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_source_directory'
|
||||
).and_return('/source')
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_runtime_directory'
|
||||
).and_return('/runtime')
|
||||
flexmock(module).should_receive('make_bootstrap_config').and_return({})
|
||||
bootstrap_arguments = flexmock(
|
||||
repository='repo',
|
||||
@@ -33,6 +30,12 @@ def test_get_config_paths_returns_list_of_config_paths():
|
||||
dry_run=False,
|
||||
)
|
||||
local_borg_version = flexmock()
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
flexmock()
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'make_runtime_directory_glob'
|
||||
).replace_with(lambda path: path)
|
||||
extract_process = flexmock(
|
||||
stdout=flexmock(
|
||||
read=lambda: '{"config_paths": ["/borgmatic/config.yaml"]}',
|
||||
@@ -47,13 +50,61 @@ def test_get_config_paths_returns_list_of_config_paths():
|
||||
) == ['/borgmatic/config.yaml']
|
||||
|
||||
|
||||
def test_get_config_paths_probes_for_manifest():
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_source_directory'
|
||||
).and_return('/source')
|
||||
flexmock(module).should_receive('make_bootstrap_config').and_return({})
|
||||
bootstrap_arguments = flexmock(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
ssh_command=None,
|
||||
local_path='borg7',
|
||||
remote_path='borg8',
|
||||
borgmatic_source_directory=None,
|
||||
user_runtime_directory=None,
|
||||
)
|
||||
global_arguments = flexmock(
|
||||
dry_run=False,
|
||||
)
|
||||
local_borg_version = flexmock()
|
||||
borgmatic_runtime_directory = flexmock()
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
borgmatic_runtime_directory,
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'make_runtime_directory_glob'
|
||||
).replace_with(lambda path: path)
|
||||
flexmock(module.os.path).should_receive('join').with_args(
|
||||
'borgmatic', 'bootstrap', 'manifest.json'
|
||||
).and_return('borgmatic/bootstrap/manifest.json').once()
|
||||
flexmock(module.os.path).should_receive('join').with_args(
|
||||
borgmatic_runtime_directory, 'bootstrap', 'manifest.json'
|
||||
).and_return('run/borgmatic/bootstrap/manifest.json').once()
|
||||
flexmock(module.os.path).should_receive('join').with_args(
|
||||
'/source', 'bootstrap', 'manifest.json'
|
||||
).and_return('/source/bootstrap/manifest.json').once()
|
||||
manifest_missing_extract_process = flexmock(
|
||||
stdout=flexmock(read=lambda: None),
|
||||
)
|
||||
manifest_found_extract_process = flexmock(
|
||||
stdout=flexmock(
|
||||
read=lambda: '{"config_paths": ["/borgmatic/config.yaml"]}',
|
||||
),
|
||||
)
|
||||
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
|
||||
manifest_missing_extract_process
|
||||
).and_return(manifest_missing_extract_process).and_return(manifest_found_extract_process)
|
||||
|
||||
assert module.get_config_paths(
|
||||
'archive', bootstrap_arguments, global_arguments, local_borg_version
|
||||
) == ['/borgmatic/config.yaml']
|
||||
|
||||
|
||||
def test_get_config_paths_translates_ssh_command_argument_to_config():
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_source_directory'
|
||||
).and_return('/source')
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_runtime_directory'
|
||||
).and_return('/runtime')
|
||||
config = flexmock()
|
||||
flexmock(module).should_receive('make_bootstrap_config').and_return(config)
|
||||
bootstrap_arguments = flexmock(
|
||||
@@ -69,6 +120,12 @@ def test_get_config_paths_translates_ssh_command_argument_to_config():
|
||||
dry_run=False,
|
||||
)
|
||||
local_borg_version = flexmock()
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
flexmock()
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'make_runtime_directory_glob'
|
||||
).replace_with(lambda path: path)
|
||||
extract_process = flexmock(
|
||||
stdout=flexmock(
|
||||
read=lambda: '{"config_paths": ["/borgmatic/config.yaml"]}',
|
||||
@@ -96,9 +153,6 @@ def test_get_config_paths_with_missing_manifest_raises_value_error():
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_source_directory'
|
||||
).and_return('/source')
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_runtime_directory'
|
||||
).and_return('/runtime')
|
||||
flexmock(module).should_receive('make_bootstrap_config').and_return({})
|
||||
bootstrap_arguments = flexmock(
|
||||
repository='repo',
|
||||
@@ -113,6 +167,13 @@ def test_get_config_paths_with_missing_manifest_raises_value_error():
|
||||
dry_run=False,
|
||||
)
|
||||
local_borg_version = flexmock()
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
flexmock()
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'make_runtime_directory_glob'
|
||||
).replace_with(lambda path: path)
|
||||
flexmock(module.os.path).should_receive('join').and_return('run/borgmatic')
|
||||
extract_process = flexmock(stdout=flexmock(read=lambda: ''))
|
||||
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
|
||||
extract_process
|
||||
@@ -128,9 +189,6 @@ def test_get_config_paths_with_broken_json_raises_value_error():
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_source_directory'
|
||||
).and_return('/source')
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_runtime_directory'
|
||||
).and_return('/runtime')
|
||||
flexmock(module).should_receive('make_bootstrap_config').and_return({})
|
||||
bootstrap_arguments = flexmock(
|
||||
repository='repo',
|
||||
@@ -145,6 +203,12 @@ def test_get_config_paths_with_broken_json_raises_value_error():
|
||||
dry_run=False,
|
||||
)
|
||||
local_borg_version = flexmock()
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
flexmock()
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'make_runtime_directory_glob'
|
||||
).replace_with(lambda path: path)
|
||||
extract_process = flexmock(
|
||||
stdout=flexmock(read=lambda: '{"config_paths": ["/oops'),
|
||||
)
|
||||
@@ -162,9 +226,6 @@ def test_get_config_paths_with_json_missing_key_raises_value_error():
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_source_directory'
|
||||
).and_return('/source')
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_runtime_directory'
|
||||
).and_return('/runtime')
|
||||
flexmock(module).should_receive('make_bootstrap_config').and_return({})
|
||||
bootstrap_arguments = flexmock(
|
||||
repository='repo',
|
||||
@@ -179,6 +240,12 @@ def test_get_config_paths_with_json_missing_key_raises_value_error():
|
||||
dry_run=False,
|
||||
)
|
||||
local_borg_version = flexmock()
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
flexmock()
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'make_runtime_directory_glob'
|
||||
).replace_with(lambda path: path)
|
||||
extract_process = flexmock(
|
||||
stdout=flexmock(read=lambda: '{}'),
|
||||
)
|
||||
@@ -210,6 +277,12 @@ def test_run_bootstrap_does_not_raise():
|
||||
dry_run=False,
|
||||
)
|
||||
local_borg_version = flexmock()
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
flexmock()
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'make_runtime_directory_glob'
|
||||
).replace_with(lambda path: path)
|
||||
extract_process = flexmock(
|
||||
stdout=flexmock(
|
||||
read=lambda: '{"config_paths": ["borgmatic/config.yaml"]}',
|
||||
@@ -244,6 +317,12 @@ def test_run_bootstrap_translates_ssh_command_argument_to_config():
|
||||
dry_run=False,
|
||||
)
|
||||
local_borg_version = flexmock()
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
flexmock()
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'make_runtime_directory_glob'
|
||||
).replace_with(lambda path: path)
|
||||
extract_process = flexmock(
|
||||
stdout=flexmock(
|
||||
read=lambda: '{"config_paths": ["borgmatic/config.yaml"]}',
|
||||
|
||||
@@ -381,7 +381,7 @@ def test_upgrade_check_times_moves_checks_from_borgmatic_source_directory_to_sta
|
||||
'/home/user/.local/state/borgmatic/checks'
|
||||
).and_return(False)
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
flexmock(module.os).should_receive('rename').with_args(
|
||||
flexmock(module.shutil).should_receive('move').with_args(
|
||||
'/home/user/.borgmatic/checks', '/home/user/.local/state/borgmatic/checks'
|
||||
).once()
|
||||
|
||||
@@ -408,13 +408,13 @@ def test_upgrade_check_times_with_checks_already_in_borgmatic_state_directory_do
|
||||
'/home/user/.local/state/borgmatic/checks'
|
||||
).and_return(True)
|
||||
flexmock(module.os).should_receive('makedirs').never()
|
||||
flexmock(module.os).should_receive('rename').never()
|
||||
flexmock(module.shutil).should_receive('move').never()
|
||||
|
||||
flexmock(module).should_receive('make_check_time_path').and_return(
|
||||
'/home/user/.local/state/borgmatic/checks/1234/archives/all'
|
||||
)
|
||||
flexmock(module.os.path).should_receive('isfile').and_return(False)
|
||||
flexmock(module.os).should_receive('rename').never()
|
||||
flexmock(module.shutil).should_receive('move').never()
|
||||
flexmock(module.os).should_receive('mkdir').never()
|
||||
|
||||
module.upgrade_check_times(flexmock(), flexmock())
|
||||
@@ -448,11 +448,11 @@ def test_upgrade_check_times_renames_old_check_paths_to_all():
|
||||
flexmock(module.os.path).should_receive('isfile').with_args(
|
||||
f'{base_path}/data.temp'
|
||||
).and_return(False)
|
||||
flexmock(module.os).should_receive('rename').with_args(
|
||||
flexmock(module.shutil).should_receive('move').with_args(
|
||||
f'{base_path}/archives', f'{base_path}/archives.temp'
|
||||
).once()
|
||||
flexmock(module.os).should_receive('mkdir').with_args(f'{base_path}/archives').once()
|
||||
flexmock(module.os).should_receive('rename').with_args(
|
||||
flexmock(module.shutil).should_receive('move').with_args(
|
||||
f'{base_path}/archives.temp', f'{base_path}/archives/all'
|
||||
).once()
|
||||
|
||||
@@ -484,11 +484,11 @@ def test_upgrade_check_times_renames_data_check_paths_when_archives_paths_are_al
|
||||
flexmock(module.os.path).should_receive('isfile').with_args(f'{base_path}/data').and_return(
|
||||
True
|
||||
)
|
||||
flexmock(module.os).should_receive('rename').with_args(
|
||||
flexmock(module.shutil).should_receive('move').with_args(
|
||||
f'{base_path}/data', f'{base_path}/data.temp'
|
||||
).once()
|
||||
flexmock(module.os).should_receive('mkdir').with_args(f'{base_path}/data').once()
|
||||
flexmock(module.os).should_receive('rename').with_args(
|
||||
flexmock(module.shutil).should_receive('move').with_args(
|
||||
f'{base_path}/data.temp', f'{base_path}/data/all'
|
||||
).once()
|
||||
|
||||
@@ -508,7 +508,7 @@ def test_upgrade_check_times_skips_already_upgraded_check_paths():
|
||||
'/home/user/.local/state/borgmatic/checks/1234/archives/all'
|
||||
)
|
||||
flexmock(module.os.path).should_receive('isfile').and_return(False)
|
||||
flexmock(module.os).should_receive('rename').never()
|
||||
flexmock(module.shutil).should_receive('move').never()
|
||||
flexmock(module.os).should_receive('mkdir').never()
|
||||
|
||||
module.upgrade_check_times(flexmock(), flexmock())
|
||||
@@ -542,11 +542,11 @@ def test_upgrade_check_times_renames_stale_temporary_check_path():
|
||||
flexmock(module.os.path).should_receive('isfile').with_args(
|
||||
f'{base_path}/data.temp'
|
||||
).and_return(False)
|
||||
flexmock(module.os).should_receive('rename').with_args(
|
||||
flexmock(module.shutil).should_receive('move').with_args(
|
||||
f'{base_path}/archives', f'{base_path}/archives.temp'
|
||||
).and_raise(FileNotFoundError)
|
||||
flexmock(module.os).should_receive('mkdir').with_args(f'{base_path}/archives').once()
|
||||
flexmock(module.os).should_receive('rename').with_args(
|
||||
flexmock(module.shutil).should_receive('move').with_args(
|
||||
f'{base_path}/archives.temp', f'{base_path}/archives/all'
|
||||
).once()
|
||||
|
||||
@@ -557,14 +557,17 @@ def test_collect_spot_check_source_paths_parses_borg_output():
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return(
|
||||
{'hook1': False, 'hook2': True}
|
||||
)
|
||||
flexmock(module.borgmatic.actions.create).should_receive(
|
||||
'process_source_directories'
|
||||
).and_return(['foo', 'bar'])
|
||||
flexmock(module.borgmatic.borg.create).should_receive('make_base_create_command').with_args(
|
||||
dry_run=True,
|
||||
repository_path='repo',
|
||||
config=object,
|
||||
config_paths=(),
|
||||
source_directories=['foo', 'bar'],
|
||||
local_borg_version=object,
|
||||
global_arguments=object,
|
||||
borgmatic_runtime_directories=(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
local_path=object,
|
||||
remote_path=object,
|
||||
list_files=True,
|
||||
@@ -588,6 +591,7 @@ def test_collect_spot_check_source_paths_parses_borg_output():
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
) == ('/etc/path', '/etc/other')
|
||||
|
||||
|
||||
@@ -595,14 +599,17 @@ 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}
|
||||
)
|
||||
flexmock(module.borgmatic.actions.create).should_receive(
|
||||
'process_source_directories'
|
||||
).and_return(['foo', 'bar'])
|
||||
flexmock(module.borgmatic.borg.create).should_receive('make_base_create_command').with_args(
|
||||
dry_run=True,
|
||||
repository_path='repo',
|
||||
config=object,
|
||||
config_paths=(),
|
||||
source_directories=['foo', 'bar'],
|
||||
local_borg_version=object,
|
||||
global_arguments=object,
|
||||
borgmatic_runtime_directories=(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
local_path=object,
|
||||
remote_path=object,
|
||||
list_files=True,
|
||||
@@ -626,6 +633,7 @@ def test_collect_spot_check_source_paths_passes_through_stream_processes_false()
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
) == ('/etc/path', '/etc/other')
|
||||
|
||||
|
||||
@@ -633,14 +641,17 @@ def test_collect_spot_check_source_paths_without_working_directory_parses_borg_o
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return(
|
||||
{'hook1': False, 'hook2': True}
|
||||
)
|
||||
flexmock(module.borgmatic.actions.create).should_receive(
|
||||
'process_source_directories'
|
||||
).and_return(['foo', 'bar'])
|
||||
flexmock(module.borgmatic.borg.create).should_receive('make_base_create_command').with_args(
|
||||
dry_run=True,
|
||||
repository_path='repo',
|
||||
config=object,
|
||||
config_paths=(),
|
||||
source_directories=['foo', 'bar'],
|
||||
local_borg_version=object,
|
||||
global_arguments=object,
|
||||
borgmatic_runtime_directories=(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
local_path=object,
|
||||
remote_path=object,
|
||||
list_files=True,
|
||||
@@ -664,6 +675,7 @@ def test_collect_spot_check_source_paths_without_working_directory_parses_borg_o
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
) == ('/etc/path', '/etc/other')
|
||||
|
||||
|
||||
@@ -671,14 +683,17 @@ def test_collect_spot_check_source_paths_skips_directories():
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return(
|
||||
{'hook1': False, 'hook2': True}
|
||||
)
|
||||
flexmock(module.borgmatic.actions.create).should_receive(
|
||||
'process_source_directories'
|
||||
).and_return(['foo', 'bar'])
|
||||
flexmock(module.borgmatic.borg.create).should_receive('make_base_create_command').with_args(
|
||||
dry_run=True,
|
||||
repository_path='repo',
|
||||
config=object,
|
||||
config_paths=(),
|
||||
source_directories=['foo', 'bar'],
|
||||
local_borg_version=object,
|
||||
global_arguments=object,
|
||||
borgmatic_runtime_directories=(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
local_path=object,
|
||||
remote_path=object,
|
||||
list_files=True,
|
||||
@@ -704,6 +719,7 @@ def test_collect_spot_check_source_paths_skips_directories():
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
== ()
|
||||
)
|
||||
@@ -713,9 +729,6 @@ def test_collect_spot_check_archive_paths_excludes_directories():
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_source_directory'
|
||||
).and_return('/home/user/.borgmatic')
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_runtime_directory'
|
||||
).and_return('/run/user/1001/borgmatic')
|
||||
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
|
||||
(
|
||||
'f /etc/path',
|
||||
@@ -732,6 +745,7 @@ def test_collect_spot_check_archive_paths_excludes_directories():
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
borgmatic_runtime_directory='/run/user/1001/borgmatic',
|
||||
) == ('/etc/path', '/etc/other')
|
||||
|
||||
|
||||
@@ -739,9 +753,6 @@ def test_collect_spot_check_archive_paths_excludes_file_in_borgmatic_runtime_dir
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_source_directory'
|
||||
).and_return('/root/.borgmatic')
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_runtime_directory'
|
||||
).and_return('/run/user/0/borgmatic')
|
||||
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
|
||||
(
|
||||
'f /etc/path',
|
||||
@@ -757,6 +768,7 @@ def test_collect_spot_check_archive_paths_excludes_file_in_borgmatic_runtime_dir
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
borgmatic_runtime_directory='/run/user/0/borgmatic',
|
||||
) == ('/etc/path',)
|
||||
|
||||
|
||||
@@ -764,9 +776,6 @@ def test_collect_spot_check_archive_paths_excludes_file_in_borgmatic_source_dire
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_source_directory'
|
||||
).and_return('/root/.borgmatic')
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_runtime_directory'
|
||||
).and_return('/run/user/0/borgmatic')
|
||||
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
|
||||
(
|
||||
'f /etc/path',
|
||||
@@ -782,6 +791,7 @@ def test_collect_spot_check_archive_paths_excludes_file_in_borgmatic_source_dire
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
borgmatic_runtime_directory='/run/user/0/borgmatic',
|
||||
) == ('/etc/path',)
|
||||
|
||||
|
||||
@@ -789,9 +799,6 @@ def test_collect_spot_check_archive_paths_excludes_file_in_borgmatic_runtime_dir
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_source_directory'
|
||||
).and_return('/root.borgmatic')
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_runtime_directory'
|
||||
).and_return('/run/user/0/borgmatic')
|
||||
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
|
||||
(
|
||||
'f /etc/path',
|
||||
@@ -807,6 +814,7 @@ def test_collect_spot_check_archive_paths_excludes_file_in_borgmatic_runtime_dir
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
borgmatic_runtime_directory='/run/user/0/borgmatic',
|
||||
) == ('/etc/path',)
|
||||
|
||||
|
||||
@@ -814,14 +822,17 @@ def test_collect_spot_check_source_paths_uses_working_directory():
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return(
|
||||
{'hook1': False, 'hook2': True}
|
||||
)
|
||||
flexmock(module.borgmatic.actions.create).should_receive(
|
||||
'process_source_directories'
|
||||
).and_return(['foo', 'bar'])
|
||||
flexmock(module.borgmatic.borg.create).should_receive('make_base_create_command').with_args(
|
||||
dry_run=True,
|
||||
repository_path='repo',
|
||||
config=object,
|
||||
config_paths=(),
|
||||
source_directories=['foo', 'bar'],
|
||||
local_borg_version=object,
|
||||
global_arguments=object,
|
||||
borgmatic_runtime_directories=(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
local_path=object,
|
||||
remote_path=object,
|
||||
list_files=True,
|
||||
@@ -848,6 +859,7 @@ def test_collect_spot_check_source_paths_uses_working_directory():
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
) == ('foo', 'bar')
|
||||
|
||||
|
||||
@@ -887,7 +899,7 @@ def test_compare_spot_check_hashes_returns_paths_having_failing_hashes():
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
log_label='repo',
|
||||
log_prefix='repo',
|
||||
source_paths=('/foo', '/bar', '/baz', '/quux'),
|
||||
) == ('/bar',)
|
||||
|
||||
@@ -928,7 +940,7 @@ def test_compare_spot_check_hashes_handles_data_sample_percentage_above_100():
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
log_label='repo',
|
||||
log_prefix='repo',
|
||||
source_paths=('/foo', '/bar'),
|
||||
) == ('/foo', '/bar')
|
||||
|
||||
@@ -966,7 +978,7 @@ def test_compare_spot_check_hashes_uses_xxh64sum_command_option():
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
log_label='repo',
|
||||
log_prefix='repo',
|
||||
source_paths=('/foo', '/bar', '/baz', '/quux'),
|
||||
) == ('/bar',)
|
||||
|
||||
@@ -1003,7 +1015,7 @@ def test_compare_spot_check_hashes_considers_path_missing_from_archive_as_not_ma
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
log_label='repo',
|
||||
log_prefix='repo',
|
||||
source_paths=('/foo', '/bar', '/baz', '/quux'),
|
||||
) == ('/bar',)
|
||||
|
||||
@@ -1039,7 +1051,7 @@ def test_compare_spot_check_hashes_considers_non_existent_path_as_not_matching()
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
log_label='repo',
|
||||
log_prefix='repo',
|
||||
source_paths=('/foo', '/bar', '/baz', '/quux'),
|
||||
) == ('/bar',)
|
||||
|
||||
@@ -1086,7 +1098,7 @@ def test_compare_spot_check_hashes_with_too_many_paths_feeds_them_to_commands_in
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
log_label='repo',
|
||||
log_prefix='repo',
|
||||
source_paths=('/foo', '/bar', '/baz', '/quux'),
|
||||
) == ('/quux',)
|
||||
|
||||
@@ -1129,7 +1141,7 @@ def test_compare_spot_check_hashes_uses_working_directory_to_access_source_paths
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
log_label='repo',
|
||||
log_prefix='repo',
|
||||
source_paths=('foo', 'bar', 'baz', 'quux'),
|
||||
) == ('bar',)
|
||||
|
||||
@@ -1149,6 +1161,7 @@ def test_spot_check_without_spot_configuration_errors():
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -1161,6 +1174,7 @@ def test_spot_check_without_any_configuration_errors():
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -1181,6 +1195,7 @@ def test_spot_check_data_tolerance_percenatge_greater_than_data_sample_percentag
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -1212,6 +1227,7 @@ def test_spot_check_with_count_delta_greater_than_count_tolerance_percentage_err
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -1244,6 +1260,7 @@ def test_spot_check_with_failing_percentage_greater_than_data_tolerance_percenta
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -1275,6 +1292,7 @@ def test_spot_check_with_high_enough_tolerances_does_not_raise():
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -1362,6 +1380,9 @@ def test_run_check_runs_configured_spot_check():
|
||||
flexmock(module).should_receive('make_archives_check_id').and_return(None)
|
||||
flexmock(module).should_receive('filter_checks_on_frequency').and_return({'spot'})
|
||||
flexmock(module.borgmatic.borg.check).should_receive('check_archives').never()
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
flexmock()
|
||||
)
|
||||
flexmock(module.borgmatic.actions.check).should_receive('spot_check').once()
|
||||
flexmock(module).should_receive('make_check_time_path')
|
||||
flexmock(module).should_receive('write_check_time')
|
||||
|
||||
@@ -1,20 +1,213 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.actions import create as module
|
||||
|
||||
|
||||
def test_expand_directory_with_basic_path_passes_it_through():
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('foo')
|
||||
flexmock(module.glob).should_receive('glob').and_return([])
|
||||
|
||||
paths = module.expand_directory('foo', None)
|
||||
|
||||
assert paths == ['foo']
|
||||
|
||||
|
||||
def test_expand_directory_with_glob_expands():
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('foo*')
|
||||
flexmock(module.glob).should_receive('glob').and_return(['foo', 'food'])
|
||||
|
||||
paths = module.expand_directory('foo*', None)
|
||||
|
||||
assert paths == ['foo', 'food']
|
||||
|
||||
|
||||
def test_expand_directory_with_working_directory_passes_it_through():
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('foo')
|
||||
flexmock(module.glob).should_receive('glob').with_args('/working/dir/foo').and_return([]).once()
|
||||
|
||||
paths = module.expand_directory('foo', working_directory='/working/dir')
|
||||
|
||||
assert paths == ['/working/dir/foo']
|
||||
|
||||
|
||||
def test_expand_directory_with_glob_passes_through_working_directory():
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('foo*')
|
||||
flexmock(module.glob).should_receive('glob').with_args('/working/dir/foo*').and_return(
|
||||
['/working/dir/foo', '/working/dir/food']
|
||||
).once()
|
||||
|
||||
paths = module.expand_directory('foo*', working_directory='/working/dir')
|
||||
|
||||
assert paths == ['/working/dir/foo', '/working/dir/food']
|
||||
|
||||
|
||||
def test_expand_directories_flattens_expanded_directories():
|
||||
flexmock(module).should_receive('expand_directory').with_args('~/foo', None).and_return(
|
||||
['/root/foo']
|
||||
)
|
||||
flexmock(module).should_receive('expand_directory').with_args('bar*', None).and_return(
|
||||
['bar', 'barf']
|
||||
)
|
||||
|
||||
paths = module.expand_directories(('~/foo', 'bar*'))
|
||||
|
||||
assert paths == ('/root/foo', 'bar', 'barf')
|
||||
|
||||
|
||||
def test_expand_directories_with_working_directory_passes_it_through():
|
||||
flexmock(module).should_receive('expand_directory').with_args('foo', '/working/dir').and_return(
|
||||
['/working/dir/foo']
|
||||
)
|
||||
|
||||
paths = module.expand_directories(('foo',), working_directory='/working/dir')
|
||||
|
||||
assert paths == ('/working/dir/foo',)
|
||||
|
||||
|
||||
def test_expand_directories_considers_none_as_no_directories():
|
||||
paths = module.expand_directories(None, None)
|
||||
|
||||
assert paths == ()
|
||||
|
||||
|
||||
def test_map_directories_to_devices_gives_device_id_per_path():
|
||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||
flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55))
|
||||
flexmock(module.os).should_receive('stat').with_args('/bar').and_return(flexmock(st_dev=66))
|
||||
|
||||
device_map = module.map_directories_to_devices(('/foo', '/bar'))
|
||||
|
||||
assert device_map == {
|
||||
'/foo': 55,
|
||||
'/bar': 66,
|
||||
}
|
||||
|
||||
|
||||
def test_map_directories_to_devices_with_missing_path_does_not_error():
|
||||
flexmock(module.os.path).should_receive('exists').and_return(True).and_return(False)
|
||||
flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55))
|
||||
flexmock(module.os).should_receive('stat').with_args('/bar').never()
|
||||
|
||||
device_map = module.map_directories_to_devices(('/foo', '/bar'))
|
||||
|
||||
assert device_map == {
|
||||
'/foo': 55,
|
||||
'/bar': None,
|
||||
}
|
||||
|
||||
|
||||
def test_map_directories_to_devices_uses_working_directory_to_construct_path():
|
||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||
flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55))
|
||||
flexmock(module.os).should_receive('stat').with_args('/working/dir/bar').and_return(
|
||||
flexmock(st_dev=66)
|
||||
)
|
||||
|
||||
device_map = module.map_directories_to_devices(
|
||||
('/foo', 'bar'), working_directory='/working/dir'
|
||||
)
|
||||
|
||||
assert device_map == {
|
||||
'/foo': 55,
|
||||
'bar': 66,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'directories,additional_directories,expected_directories',
|
||||
(
|
||||
({'/': 1, '/root': 1}, {}, ['/']),
|
||||
({'/': 1, '/root/': 1}, {}, ['/']),
|
||||
({'/': 1, '/root': 2}, {}, ['/', '/root']),
|
||||
({'/root': 1, '/': 1}, {}, ['/']),
|
||||
({'/root': 1, '/root/foo': 1}, {}, ['/root']),
|
||||
({'/root/': 1, '/root/foo': 1}, {}, ['/root/']),
|
||||
({'/root': 1, '/root/foo/': 1}, {}, ['/root']),
|
||||
({'/root': 1, '/root/foo': 2}, {}, ['/root', '/root/foo']),
|
||||
({'/root/foo': 1, '/root': 1}, {}, ['/root']),
|
||||
({'/root': None, '/root/foo': None}, {}, ['/root', '/root/foo']),
|
||||
({'/root': 1, '/etc': 1, '/root/foo/bar': 1}, {}, ['/etc', '/root']),
|
||||
({'/root': 1, '/root/foo': 1, '/root/foo/bar': 1}, {}, ['/root']),
|
||||
({'/dup': 1, '/dup': 1}, {}, ['/dup']),
|
||||
({'/foo': 1, '/bar': 1}, {}, ['/bar', '/foo']),
|
||||
({'/foo': 1, '/bar': 2}, {}, ['/bar', '/foo']),
|
||||
({'/root/foo': 1}, {'/root': 1}, []),
|
||||
({'/root/foo': 1}, {'/root': 2}, ['/root/foo']),
|
||||
({'/root/foo': 1}, {}, ['/root/foo']),
|
||||
),
|
||||
)
|
||||
def test_deduplicate_directories_removes_child_paths_on_the_same_filesystem(
|
||||
directories, additional_directories, expected_directories
|
||||
):
|
||||
assert (
|
||||
module.deduplicate_directories(directories, additional_directories) == expected_directories
|
||||
)
|
||||
|
||||
|
||||
def test_pattern_root_directories_deals_with_none_patterns():
|
||||
assert module.pattern_root_directories(patterns=None) == []
|
||||
|
||||
|
||||
def test_pattern_root_directories_parses_roots_and_ignores_others():
|
||||
assert module.pattern_root_directories(
|
||||
['R /root', '+ /root/foo', '- /root/foo/bar', 'R /baz']
|
||||
) == ['/root', '/baz']
|
||||
|
||||
|
||||
def test_process_source_directories_includes_source_directories():
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
|
||||
'/working'
|
||||
)
|
||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||
flexmock(module).should_receive('expand_directories').with_args(
|
||||
('foo', 'bar'), working_directory='/working'
|
||||
).and_return(()).once()
|
||||
flexmock(module).should_receive('pattern_root_directories').and_return(())
|
||||
flexmock(module).should_receive('expand_directories').with_args(
|
||||
(), working_directory='/working'
|
||||
).and_return(())
|
||||
|
||||
assert module.process_source_directories(
|
||||
config={'source_directories': ['foo', 'bar']},
|
||||
) == ('foo', 'bar')
|
||||
|
||||
|
||||
def test_process_source_directories_prefers_source_directory_argument_to_config():
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
|
||||
'/working'
|
||||
)
|
||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||
flexmock(module).should_receive('expand_directories').with_args(
|
||||
('foo', 'bar'), working_directory='/working'
|
||||
).and_return(()).once()
|
||||
flexmock(module).should_receive('pattern_root_directories').and_return(())
|
||||
flexmock(module).should_receive('expand_directories').with_args(
|
||||
(), working_directory='/working'
|
||||
).and_return(())
|
||||
|
||||
assert module.process_source_directories(
|
||||
config={'source_directories': ['nope']},
|
||||
source_directories=['foo', 'bar'],
|
||||
) == ('foo', 'bar')
|
||||
|
||||
|
||||
def test_run_create_executes_and_calls_hooks_for_configured_repository():
|
||||
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).should_receive('create_borgmatic_manifest').once()
|
||||
flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return({})
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive(
|
||||
'call_hooks_even_if_unconfigured'
|
||||
).and_return({})
|
||||
flexmock(module).should_receive('process_source_directories').and_return([])
|
||||
flexmock(module.os.path).should_receive('join').and_return('/run/borgmatic/bootstrap')
|
||||
create_arguments = flexmock(
|
||||
repository=None,
|
||||
progress=flexmock(),
|
||||
@@ -41,54 +234,22 @@ def test_run_create_executes_and_calls_hooks_for_configured_repository():
|
||||
)
|
||||
|
||||
|
||||
def test_run_create_with_store_config_files_false_does_not_create_borgmatic_manifest():
|
||||
flexmock(module.logger).answer = lambda message: None
|
||||
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
|
||||
flexmock(module.borgmatic.borg.create).should_receive('create_archive').once()
|
||||
flexmock(module).should_receive('create_borgmatic_manifest').never()
|
||||
flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return({})
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive(
|
||||
'call_hooks_even_if_unconfigured'
|
||||
).and_return({})
|
||||
create_arguments = flexmock(
|
||||
repository=None,
|
||||
progress=flexmock(),
|
||||
stats=flexmock(),
|
||||
json=False,
|
||||
list_files=flexmock(),
|
||||
)
|
||||
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
|
||||
|
||||
list(
|
||||
module.run_create(
|
||||
config_filename='test.yaml',
|
||||
repository={'path': 'repo'},
|
||||
config={'store_config_files': False},
|
||||
config_paths=['/tmp/test.yaml'],
|
||||
hook_context={},
|
||||
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_runs_with_selected_repository():
|
||||
flexmock(module.logger).answer = lambda message: None
|
||||
flexmock(module.borgmatic.config.validate).should_receive(
|
||||
'repositories_match'
|
||||
).once().and_return(True)
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
flexmock()
|
||||
)
|
||||
flexmock(module.borgmatic.borg.create).should_receive('create_archive').once()
|
||||
flexmock(module).should_receive('create_borgmatic_manifest').once()
|
||||
flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return({})
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive(
|
||||
'call_hooks_even_if_unconfigured'
|
||||
).and_return({})
|
||||
flexmock(module).should_receive('process_source_directories').and_return([])
|
||||
flexmock(module.os.path).should_receive('join').and_return('/run/borgmatic/bootstrap')
|
||||
create_arguments = flexmock(
|
||||
repository=flexmock(),
|
||||
progress=flexmock(),
|
||||
@@ -120,8 +281,8 @@ def test_run_create_bails_if_repository_does_not_match():
|
||||
flexmock(module.borgmatic.config.validate).should_receive(
|
||||
'repositories_match'
|
||||
).once().and_return(False)
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').never()
|
||||
flexmock(module.borgmatic.borg.create).should_receive('create_archive').never()
|
||||
flexmock(module).should_receive('create_borgmatic_manifest').never()
|
||||
create_arguments = flexmock(
|
||||
repository=flexmock(),
|
||||
progress=flexmock(),
|
||||
@@ -153,17 +314,21 @@ def test_run_create_produces_json():
|
||||
flexmock(module.borgmatic.config.validate).should_receive(
|
||||
'repositories_match'
|
||||
).once().and_return(True)
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
flexmock()
|
||||
)
|
||||
flexmock(module.borgmatic.borg.create).should_receive('create_archive').once().and_return(
|
||||
flexmock()
|
||||
)
|
||||
parsed_json = flexmock()
|
||||
flexmock(module.borgmatic.actions.json).should_receive('parse_json').and_return(parsed_json)
|
||||
flexmock(module).should_receive('create_borgmatic_manifest').once()
|
||||
flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return({})
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive(
|
||||
'call_hooks_even_if_unconfigured'
|
||||
).and_return({})
|
||||
flexmock(module).should_receive('process_source_directories').and_return([])
|
||||
flexmock(module.os.path).should_receive('join').and_return('/run/borgmatic/bootstrap')
|
||||
create_arguments = flexmock(
|
||||
repository=flexmock(),
|
||||
progress=flexmock(),
|
||||
@@ -188,57 +353,3 @@ def test_run_create_produces_json():
|
||||
remote_path=None,
|
||||
)
|
||||
) == [parsed_json]
|
||||
|
||||
|
||||
def test_create_borgmatic_manifest_creates_manifest_file():
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_runtime_directory'
|
||||
).and_return('/run/user/0/borgmatic')
|
||||
flexmock(module.os.path).should_receive('join').with_args(
|
||||
'/run/user/0/borgmatic', 'bootstrap', 'manifest.json'
|
||||
).and_return('/run/user/0/borgmatic/bootstrap/manifest.json')
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.os).should_receive('makedirs').and_return(True)
|
||||
|
||||
flexmock(module.importlib.metadata).should_receive('version').and_return('1.0.0')
|
||||
flexmock(sys.modules['builtins']).should_receive('open').with_args(
|
||||
'/run/user/0/borgmatic/bootstrap/manifest.json', 'w'
|
||||
).and_return(
|
||||
flexmock(
|
||||
__enter__=lambda *args: flexmock(write=lambda *args: None, close=lambda *args: None),
|
||||
__exit__=lambda *args: None,
|
||||
)
|
||||
)
|
||||
flexmock(module.json).should_receive('dump').and_return(True).once()
|
||||
|
||||
module.create_borgmatic_manifest({}, 'test.yaml', False)
|
||||
|
||||
|
||||
def test_create_borgmatic_manifest_creates_manifest_file_with_custom_borgmatic_runtime_directory():
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_runtime_directory'
|
||||
).and_return('/borgmatic')
|
||||
flexmock(module.os.path).should_receive('join').with_args(
|
||||
'/borgmatic', 'bootstrap', 'manifest.json'
|
||||
).and_return('/borgmatic/bootstrap/manifest.json')
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.os).should_receive('makedirs').and_return(True)
|
||||
|
||||
flexmock(module.importlib.metadata).should_receive('version').and_return('1.0.0')
|
||||
flexmock(sys.modules['builtins']).should_receive('open').with_args(
|
||||
'/borgmatic/bootstrap/manifest.json', 'w'
|
||||
).and_return(
|
||||
flexmock(
|
||||
__enter__=lambda *args: flexmock(write=lambda *args: None, close=lambda *args: None),
|
||||
__exit__=lambda *args: None,
|
||||
)
|
||||
)
|
||||
flexmock(module.json).should_receive('dump').and_return(True).once()
|
||||
|
||||
module.create_borgmatic_manifest(
|
||||
{'borgmatic_runtime_directory': '/borgmatic'}, 'test.yaml', False
|
||||
)
|
||||
|
||||
|
||||
def test_create_borgmatic_manifest_does_not_create_manifest_file_on_dry_run():
|
||||
module.create_borgmatic_manifest({}, 'test.yaml', True)
|
||||
|
||||
@@ -75,10 +75,10 @@ def test_strip_path_prefix_from_extracted_dump_destination_renames_first_matchin
|
||||
]
|
||||
)
|
||||
|
||||
flexmock(module.os).should_receive('rename').with_args(
|
||||
flexmock(module.shutil).should_receive('move').with_args(
|
||||
'/foo/bar/postgresql_databases', '/run/user/0/borgmatic/postgresql_databases'
|
||||
).once()
|
||||
flexmock(module.os).should_receive('rename').with_args(
|
||||
flexmock(module.shutil).should_receive('move').with_args(
|
||||
'/foo/bar/mariadb_databases', '/run/user/0/borgmatic/mariadb_databases'
|
||||
).never()
|
||||
|
||||
@@ -87,11 +87,8 @@ def test_strip_path_prefix_from_extracted_dump_destination_renames_first_matchin
|
||||
|
||||
def test_restore_single_data_source_extracts_and_restores_single_file_dump():
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').with_args(
|
||||
'make_data_source_dump_patterns', object, object, object, object
|
||||
'make_data_source_dump_patterns', object, object, object, object, object
|
||||
).and_return({'postgresql': flexmock()})
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_runtime_directory'
|
||||
).and_return('/run/user/0/borgmatic')
|
||||
flexmock(module.tempfile).should_receive('mkdtemp').never()
|
||||
flexmock(module.borgmatic.hooks.dump).should_receive(
|
||||
'convert_glob_patterns_to_borg_pattern'
|
||||
@@ -110,6 +107,7 @@ def test_restore_single_data_source_extracts_and_restores_single_file_dump():
|
||||
dry_run=object,
|
||||
extract_process=object,
|
||||
connection_params=object,
|
||||
borgmatic_runtime_directory=object,
|
||||
).once()
|
||||
|
||||
module.restore_single_data_source(
|
||||
@@ -123,16 +121,14 @@ def test_restore_single_data_source_extracts_and_restores_single_file_dump():
|
||||
hook_name='postgresql',
|
||||
data_source={'name': 'test', 'format': 'plain'},
|
||||
connection_params=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
def test_restore_single_data_source_extracts_and_restores_directory_dump():
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').with_args(
|
||||
'make_data_source_dump_patterns', object, object, object, object
|
||||
'make_data_source_dump_patterns', object, object, object, object, object
|
||||
).and_return({'postgresql': flexmock()})
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_runtime_directory'
|
||||
).and_return('/run/user/0/borgmatic')
|
||||
flexmock(module.tempfile).should_receive('mkdtemp').once().and_return(
|
||||
'/run/user/0/borgmatic/tmp1234'
|
||||
)
|
||||
@@ -153,6 +149,7 @@ def test_restore_single_data_source_extracts_and_restores_directory_dump():
|
||||
dry_run=object,
|
||||
extract_process=object,
|
||||
connection_params=object,
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
).once()
|
||||
|
||||
module.restore_single_data_source(
|
||||
@@ -166,16 +163,14 @@ def test_restore_single_data_source_extracts_and_restores_directory_dump():
|
||||
hook_name='postgresql',
|
||||
data_source={'name': 'test', 'format': 'directory'},
|
||||
connection_params=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
def test_restore_single_data_source_with_directory_dump_error_cleans_up_temporary_directory():
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').with_args(
|
||||
'make_data_source_dump_patterns', object, object, object, object
|
||||
'make_data_source_dump_patterns', object, object, object, object, object
|
||||
).and_return({'postgresql': flexmock()})
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_runtime_directory'
|
||||
).and_return('/run/user/0/borgmatic')
|
||||
flexmock(module.tempfile).should_receive('mkdtemp').once().and_return(
|
||||
'/run/user/0/borgmatic/tmp1234'
|
||||
)
|
||||
@@ -196,6 +191,7 @@ def test_restore_single_data_source_with_directory_dump_error_cleans_up_temporar
|
||||
dry_run=object,
|
||||
extract_process=object,
|
||||
connection_params=object,
|
||||
borgmatic_runtime_directory='/run/user/0/borgmatic/tmp1234',
|
||||
).never()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
@@ -210,19 +206,15 @@ def test_restore_single_data_source_with_directory_dump_error_cleans_up_temporar
|
||||
hook_name='postgresql',
|
||||
data_source={'name': 'test', 'format': 'directory'},
|
||||
connection_params=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
def test_restore_single_data_source_with_directory_dump_and_dry_run_skips_directory_move_and_cleanup():
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').with_args(
|
||||
'make_data_source_dump_patterns', object, object, object, object
|
||||
'make_data_source_dump_patterns', object, object, object, object, object
|
||||
).and_return({'postgresql': flexmock()})
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_runtime_directory'
|
||||
).and_return('/run/user/0/borgmatic')
|
||||
flexmock(module.tempfile).should_receive('mkdtemp').once().and_return(
|
||||
'/run/user/0/borgmatic/tmp1234'
|
||||
)
|
||||
flexmock(module.tempfile).should_receive('mkdtemp').once().and_return('/run/borgmatic/tmp1234')
|
||||
flexmock(module.borgmatic.hooks.dump).should_receive(
|
||||
'convert_glob_patterns_to_borg_pattern'
|
||||
).and_return(flexmock())
|
||||
@@ -240,6 +232,7 @@ def test_restore_single_data_source_with_directory_dump_and_dry_run_skips_direct
|
||||
dry_run=object,
|
||||
extract_process=object,
|
||||
connection_params=object,
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
).once()
|
||||
|
||||
module.restore_single_data_source(
|
||||
@@ -253,6 +246,7 @@ def test_restore_single_data_source_with_directory_dump_and_dry_run_skips_direct
|
||||
hook_name='postgresql',
|
||||
data_source={'name': 'test', 'format': 'directory'},
|
||||
connection_params=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -260,9 +254,6 @@ def test_collect_archive_data_source_names_parses_archive_paths():
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_source_directory'
|
||||
).and_return('/root/.borgmatic')
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_runtime_directory'
|
||||
).and_return('/run/user/0/borgmatic')
|
||||
flexmock(module.borgmatic.hooks.dump).should_receive('make_data_source_dump_path').and_return(
|
||||
''
|
||||
)
|
||||
@@ -282,6 +273,7 @@ def test_collect_archive_data_source_names_parses_archive_paths():
|
||||
global_arguments=flexmock(log_json=False),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
assert archive_data_source_names == {
|
||||
@@ -294,9 +286,6 @@ def test_collect_archive_data_source_names_parses_archive_paths_with_different_b
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_source_directory'
|
||||
).and_return('/root/.borgmatic')
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_runtime_directory'
|
||||
).and_return('/run/user/0/borgmatic')
|
||||
flexmock(module.borgmatic.hooks.dump).should_receive('make_data_source_dump_path').and_return(
|
||||
''
|
||||
)
|
||||
@@ -317,6 +306,7 @@ def test_collect_archive_data_source_names_parses_archive_paths_with_different_b
|
||||
global_arguments=flexmock(log_json=False),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
assert archive_data_source_names == {
|
||||
@@ -329,9 +319,6 @@ def test_collect_archive_data_source_names_parses_directory_format_archive_paths
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_source_directory'
|
||||
).and_return('/root/.borgmatic')
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_runtime_directory'
|
||||
).and_return('/run/user/0/borgmatic')
|
||||
flexmock(module.borgmatic.hooks.dump).should_receive('make_data_source_dump_path').and_return(
|
||||
''
|
||||
)
|
||||
@@ -350,6 +337,7 @@ def test_collect_archive_data_source_names_parses_directory_format_archive_paths
|
||||
global_arguments=flexmock(log_json=False),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
assert archive_data_source_names == {
|
||||
@@ -361,9 +349,6 @@ def test_collect_archive_data_source_names_skips_bad_archive_paths():
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_source_directory'
|
||||
).and_return('/root/.borgmatic')
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'get_borgmatic_runtime_directory'
|
||||
).and_return('/run/user/0/borgmatic')
|
||||
flexmock(module.borgmatic.hooks.dump).should_receive('make_data_source_dump_path').and_return(
|
||||
''
|
||||
)
|
||||
@@ -384,6 +369,7 @@ def test_collect_archive_data_source_names_skips_bad_archive_paths():
|
||||
global_arguments=flexmock(log_json=False),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
assert archive_data_source_names == {
|
||||
@@ -481,6 +467,13 @@ def test_run_restore_restores_each_data_source():
|
||||
}
|
||||
|
||||
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
|
||||
borgmatic_runtime_directory = flexmock()
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
borgmatic_runtime_directory
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'make_runtime_directory_glob'
|
||||
).replace_with(lambda path: path)
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks_even_if_unconfigured')
|
||||
flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
|
||||
flexmock()
|
||||
@@ -501,6 +494,7 @@ def test_run_restore_restores_each_data_source():
|
||||
hook_name='postgresql_databases',
|
||||
data_source={'name': 'foo', 'schemas': None},
|
||||
connection_params=object,
|
||||
borgmatic_runtime_directory=borgmatic_runtime_directory,
|
||||
).once()
|
||||
flexmock(module).should_receive('restore_single_data_source').with_args(
|
||||
repository=object,
|
||||
@@ -513,6 +507,7 @@ def test_run_restore_restores_each_data_source():
|
||||
hook_name='postgresql_databases',
|
||||
data_source={'name': 'bar', 'schemas': None},
|
||||
connection_params=object,
|
||||
borgmatic_runtime_directory=borgmatic_runtime_directory,
|
||||
).once()
|
||||
flexmock(module).should_receive('ensure_data_sources_found')
|
||||
|
||||
@@ -541,6 +536,12 @@ def test_run_restore_bails_for_non_matching_repository():
|
||||
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(
|
||||
False
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
flexmock()
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'make_runtime_directory_glob'
|
||||
).replace_with(lambda path: path)
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive(
|
||||
'call_hooks_even_if_unconfigured'
|
||||
).never()
|
||||
@@ -563,6 +564,13 @@ def test_run_restore_restores_data_source_configured_with_all_name():
|
||||
}
|
||||
|
||||
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
|
||||
borgmatic_runtime_directory = flexmock()
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
borgmatic_runtime_directory
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'make_runtime_directory_glob'
|
||||
).replace_with(lambda path: path)
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks_even_if_unconfigured')
|
||||
flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
|
||||
flexmock()
|
||||
@@ -599,6 +607,7 @@ def test_run_restore_restores_data_source_configured_with_all_name():
|
||||
hook_name='postgresql_databases',
|
||||
data_source={'name': 'foo', 'schemas': None},
|
||||
connection_params=object,
|
||||
borgmatic_runtime_directory=borgmatic_runtime_directory,
|
||||
).once()
|
||||
flexmock(module).should_receive('restore_single_data_source').with_args(
|
||||
repository=object,
|
||||
@@ -611,6 +620,7 @@ def test_run_restore_restores_data_source_configured_with_all_name():
|
||||
hook_name='postgresql_databases',
|
||||
data_source={'name': 'bar', 'schemas': None},
|
||||
connection_params=object,
|
||||
borgmatic_runtime_directory=borgmatic_runtime_directory,
|
||||
).once()
|
||||
flexmock(module).should_receive('ensure_data_sources_found')
|
||||
|
||||
@@ -641,6 +651,13 @@ def test_run_restore_skips_missing_data_source():
|
||||
}
|
||||
|
||||
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
|
||||
borgmatic_runtime_directory = flexmock()
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
borgmatic_runtime_directory
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'make_runtime_directory_glob'
|
||||
).replace_with(lambda path: path)
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks_even_if_unconfigured')
|
||||
flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
|
||||
flexmock()
|
||||
@@ -677,6 +694,7 @@ def test_run_restore_skips_missing_data_source():
|
||||
hook_name='postgresql_databases',
|
||||
data_source={'name': 'foo', 'schemas': None},
|
||||
connection_params=object,
|
||||
borgmatic_runtime_directory=borgmatic_runtime_directory,
|
||||
).once()
|
||||
flexmock(module).should_receive('restore_single_data_source').with_args(
|
||||
repository=object,
|
||||
@@ -689,6 +707,7 @@ def test_run_restore_skips_missing_data_source():
|
||||
hook_name='postgresql_databases',
|
||||
data_source={'name': 'bar', 'schemas': None},
|
||||
connection_params=object,
|
||||
borgmatic_runtime_directory=borgmatic_runtime_directory,
|
||||
).never()
|
||||
flexmock(module).should_receive('ensure_data_sources_found')
|
||||
|
||||
@@ -720,6 +739,13 @@ def test_run_restore_restores_data_sources_from_different_hooks():
|
||||
}
|
||||
|
||||
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
|
||||
borgmatic_runtime_directory = flexmock()
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
borgmatic_runtime_directory
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'make_runtime_directory_glob'
|
||||
).replace_with(lambda path: path)
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks_even_if_unconfigured')
|
||||
flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
|
||||
flexmock()
|
||||
@@ -749,6 +775,7 @@ def test_run_restore_restores_data_sources_from_different_hooks():
|
||||
hook_name='postgresql_databases',
|
||||
data_source={'name': 'foo', 'schemas': None},
|
||||
connection_params=object,
|
||||
borgmatic_runtime_directory=borgmatic_runtime_directory,
|
||||
).once()
|
||||
flexmock(module).should_receive('restore_single_data_source').with_args(
|
||||
repository=object,
|
||||
@@ -761,6 +788,7 @@ def test_run_restore_restores_data_sources_from_different_hooks():
|
||||
hook_name='mysql_databases',
|
||||
data_source={'name': 'bar', 'schemas': None},
|
||||
connection_params=object,
|
||||
borgmatic_runtime_directory=borgmatic_runtime_directory,
|
||||
).once()
|
||||
flexmock(module).should_receive('ensure_data_sources_found')
|
||||
|
||||
|
||||
@@ -703,6 +703,31 @@ def test_check_archives_with_remote_path_passes_through_to_borg():
|
||||
)
|
||||
|
||||
|
||||
def test_check_archives_with_umask_passes_through_to_borg():
|
||||
checks = {'repository'}
|
||||
config = {'umask': '077'}
|
||||
flexmock(module).should_receive('make_check_name_flags').with_args(checks, ()).and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(('borg', 'check', '--umask', '077', 'repo'))
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
config=config,
|
||||
local_borg_version='1.2.3',
|
||||
check_arguments=flexmock(
|
||||
progress=None,
|
||||
repair=None,
|
||||
only_checks=None,
|
||||
force=None,
|
||||
match_archives=None,
|
||||
max_duration=None,
|
||||
),
|
||||
global_arguments=flexmock(log_json=False),
|
||||
checks=checks,
|
||||
archive_filter_flags=(),
|
||||
)
|
||||
|
||||
|
||||
def test_check_archives_with_log_json_passes_through_to_borg():
|
||||
checks = {'repository'}
|
||||
config = {}
|
||||
|
||||
+73
-555
File diff suppressed because it is too large
Load Diff
@@ -100,6 +100,29 @@ def test_make_delete_command_includes_remote_path():
|
||||
assert command == ('borg', 'delete', '--remote-path', 'borg1', 'repo')
|
||||
|
||||
|
||||
def test_make_delete_command_includes_umask():
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').replace_with(
|
||||
lambda name, value: (f'--{name}', value) if value else ()
|
||||
)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('repo',)
|
||||
)
|
||||
|
||||
command = module.make_delete_command(
|
||||
repository={'path': 'repo'},
|
||||
config={'umask': '077'},
|
||||
local_borg_version='1.2.3',
|
||||
delete_arguments=flexmock(list_archives=False, force=0, match_archives=None, archive=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert command == ('borg', 'delete', '--umask', '077', 'repo')
|
||||
|
||||
|
||||
def test_make_delete_command_includes_log_json():
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').with_args(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.borg import environment as module
|
||||
|
||||
|
||||
@@ -8,21 +10,24 @@ def test_make_environment_with_passcommand_should_set_environment():
|
||||
|
||||
|
||||
def test_make_environment_with_passphrase_should_set_environment():
|
||||
flexmock(module.os.environ).should_receive('get').and_return(None)
|
||||
environment = module.make_environment({'encryption_passphrase': 'pass'})
|
||||
|
||||
assert environment.get('BORG_PASSPHRASE') == 'pass'
|
||||
|
||||
|
||||
def test_make_environment_with_ssh_command_should_set_environment():
|
||||
flexmock(module.os.environ).should_receive('get').and_return(None)
|
||||
environment = module.make_environment({'ssh_command': 'ssh -C'})
|
||||
|
||||
assert environment.get('BORG_RSH') == 'ssh -C'
|
||||
|
||||
|
||||
def test_make_environment_without_configuration_sets_certain_environment_variables():
|
||||
flexmock(module.os.environ).should_receive('get').and_return(None)
|
||||
environment = module.make_environment({})
|
||||
|
||||
# borgmatic always sets this Borg environment variable.
|
||||
# Default environment variables.
|
||||
assert environment == {
|
||||
'BORG_EXIT_CODES': 'modern',
|
||||
'BORG_RELOCATED_REPO_ACCESS_IS_OK': 'no',
|
||||
@@ -30,30 +35,47 @@ def test_make_environment_without_configuration_sets_certain_environment_variabl
|
||||
}
|
||||
|
||||
|
||||
def test_make_environment_without_configuration_does_not_set_certain_environment_variables_if_already_set():
|
||||
flexmock(module.os.environ).should_receive('get').with_args(
|
||||
'BORG_RELOCATED_REPO_ACCESS_IS_OK'
|
||||
).and_return('yup')
|
||||
flexmock(module.os.environ).should_receive('get').with_args(
|
||||
'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK'
|
||||
).and_return('nah')
|
||||
environment = module.make_environment({})
|
||||
|
||||
assert environment == {'BORG_EXIT_CODES': 'modern'}
|
||||
|
||||
|
||||
def test_make_environment_with_relocated_repo_access_true_should_set_environment_yes():
|
||||
flexmock(module.os.environ).should_receive('get').and_return(None)
|
||||
environment = module.make_environment({'relocated_repo_access_is_ok': True})
|
||||
|
||||
assert environment.get('BORG_RELOCATED_REPO_ACCESS_IS_OK') == 'yes'
|
||||
|
||||
|
||||
def test_make_environment_with_relocated_repo_access_false_should_set_environment_no():
|
||||
flexmock(module.os.environ).should_receive('get').and_return(None)
|
||||
environment = module.make_environment({'relocated_repo_access_is_ok': False})
|
||||
|
||||
assert environment.get('BORG_RELOCATED_REPO_ACCESS_IS_OK') == 'no'
|
||||
|
||||
|
||||
def test_make_environment_check_i_know_what_i_am_doing_true_should_set_environment_YES():
|
||||
flexmock(module.os.environ).should_receive('get').and_return(None)
|
||||
environment = module.make_environment({'check_i_know_what_i_am_doing': True})
|
||||
|
||||
assert environment.get('BORG_CHECK_I_KNOW_WHAT_I_AM_DOING') == 'YES'
|
||||
|
||||
|
||||
def test_make_environment_check_i_know_what_i_am_doing_false_should_set_environment_NO():
|
||||
flexmock(module.os.environ).should_receive('get').and_return(None)
|
||||
environment = module.make_environment({'check_i_know_what_i_am_doing': False})
|
||||
|
||||
assert environment.get('BORG_CHECK_I_KNOW_WHAT_I_AM_DOING') == 'NO'
|
||||
|
||||
|
||||
def test_make_environment_with_integer_variable_value():
|
||||
flexmock(module.os.environ).should_receive('get').and_return(None)
|
||||
environment = module.make_environment({'borg_files_cache_ttl': 40})
|
||||
assert environment.get('BORG_FILES_CACHE_TTL') == '40'
|
||||
|
||||
@@ -203,6 +203,29 @@ def test_make_info_command_with_remote_path_passes_through_to_command():
|
||||
assert command == ('borg', 'info', '--remote-path', 'borg1', '--repo', 'repo')
|
||||
|
||||
|
||||
def test_make_info_command_with_umask_passes_through_to_command():
|
||||
flexmock(module.flags).should_receive('make_flags').replace_with(
|
||||
lambda name, value: (f'--{name}', value) if value else ()
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
|
||||
None, None, '2.3.4'
|
||||
).and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||
|
||||
command = module.make_info_command(
|
||||
repository_path='repo',
|
||||
config={'umask': '077'},
|
||||
local_borg_version='2.3.4',
|
||||
global_arguments=flexmock(log_json=False),
|
||||
info_arguments=flexmock(archive=None, json=False, prefix=None, match_archives=None),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert command == ('borg', 'info', '--umask', '077', '--repo', 'repo')
|
||||
|
||||
|
||||
def test_make_info_command_with_log_json_passes_through_to_command():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('log-json', True).and_return(
|
||||
|
||||
@@ -94,7 +94,9 @@ def test_make_list_command_includes_json():
|
||||
|
||||
|
||||
def test_make_list_command_includes_log_json():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(('--log-json',))
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(()).and_return(
|
||||
('--log-json',)
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
|
||||
@@ -110,7 +112,7 @@ def test_make_list_command_includes_log_json():
|
||||
|
||||
|
||||
def test_make_list_command_includes_lock_wait():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(()).and_return(
|
||||
('--lock-wait', '5')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||
@@ -203,6 +205,24 @@ def test_make_list_command_includes_remote_path():
|
||||
assert command == ('borg', 'list', '--remote-path', 'borg2', 'repo')
|
||||
|
||||
|
||||
def test_make_list_command_includes_umask():
|
||||
flexmock(module.flags).should_receive('make_flags').replace_with(
|
||||
lambda name, value: (f'--{name}', value) if value else ()
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
|
||||
command = module.make_list_command(
|
||||
repository_path='repo',
|
||||
config={'umask': '077'},
|
||||
local_borg_version='1.2.3',
|
||||
list_arguments=flexmock(archive=None, paths=None, json=False),
|
||||
global_arguments=flexmock(log_json=False),
|
||||
)
|
||||
|
||||
assert command == ('borg', 'list', '--umask', '077', 'repo')
|
||||
|
||||
|
||||
def test_make_list_command_includes_short():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--short',))
|
||||
|
||||
@@ -438,6 +438,27 @@ def test_create_repository_with_remote_path_calls_borg_with_remote_path_flag():
|
||||
)
|
||||
|
||||
|
||||
def test_create_repository_with_umask_calls_borg_with_umask_flag():
|
||||
insert_repo_info_command_not_found_mock()
|
||||
insert_repo_create_command_mock(REPO_CREATE_COMMAND + ('--umask', '077', '--repo', 'repo'))
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(
|
||||
(
|
||||
'--repo',
|
||||
'repo',
|
||||
)
|
||||
)
|
||||
|
||||
module.create_repository(
|
||||
dry_run=False,
|
||||
repository_path='repo',
|
||||
config={'umask': '077'},
|
||||
local_borg_version='2.3.4',
|
||||
global_arguments=flexmock(log_json=False),
|
||||
encryption_mode='repokey',
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
|
||||
@@ -141,6 +141,29 @@ def test_make_repo_delete_command_includes_remote_path():
|
||||
assert command == ('borg', 'repo-delete', '--remote-path', 'borg1', 'repo')
|
||||
|
||||
|
||||
def test_make_repo_delete_command_includes_umask():
|
||||
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').replace_with(
|
||||
lambda name, value: (f'--{name}', value) if value else ()
|
||||
)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('repo',)
|
||||
)
|
||||
|
||||
command = module.make_repo_delete_command(
|
||||
repository={'path': 'repo'},
|
||||
config={'umask': '077'},
|
||||
local_borg_version='1.2.3',
|
||||
repo_delete_arguments=flexmock(list_archives=False, force=0),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert command == ('borg', 'repo-delete', '--umask', '077', 'repo')
|
||||
|
||||
|
||||
def test_make_repo_delete_command_includes_log_json():
|
||||
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
|
||||
|
||||
@@ -11,6 +11,9 @@ def test_display_repository_info_calls_borg_with_flags():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_flags').replace_with(
|
||||
lambda name, value: (f'--{name}', value) if value else ()
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(
|
||||
(
|
||||
'--repo',
|
||||
@@ -49,6 +52,9 @@ def test_display_repository_info_without_borg_features_calls_borg_with_info_sub_
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module.feature).should_receive('available').and_return(False)
|
||||
flexmock(module.flags).should_receive('make_flags').replace_with(
|
||||
lambda name, value: (f'--{name}', value) if value else ()
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -82,6 +88,9 @@ def test_display_repository_info_with_log_info_calls_borg_with_info_flag():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_flags').replace_with(
|
||||
lambda name, value: (f'--{name}', value) if value else ()
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(
|
||||
(
|
||||
'--repo',
|
||||
@@ -120,6 +129,9 @@ def test_display_repository_info_with_log_info_and_json_suppresses_most_borg_out
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_flags').replace_with(
|
||||
lambda name, value: (f'--{name}', value) if value else ()
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(
|
||||
(
|
||||
'--repo',
|
||||
@@ -153,6 +165,9 @@ def test_display_repository_info_with_log_debug_calls_borg_with_debug_flag():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_flags').replace_with(
|
||||
lambda name, value: (f'--{name}', value) if value else ()
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(
|
||||
(
|
||||
'--repo',
|
||||
@@ -192,6 +207,9 @@ def test_display_repository_info_with_log_debug_and_json_suppresses_most_borg_ou
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_flags').replace_with(
|
||||
lambda name, value: (f'--{name}', value) if value else ()
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(
|
||||
(
|
||||
'--repo',
|
||||
@@ -225,6 +243,9 @@ def test_display_repository_info_with_json_calls_borg_with_json_flag():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_flags').replace_with(
|
||||
lambda name, value: (f'--{name}', value) if value else ()
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(
|
||||
(
|
||||
'--repo',
|
||||
@@ -257,6 +278,9 @@ def test_display_repository_info_with_local_path_calls_borg_via_local_path():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_flags').replace_with(
|
||||
lambda name, value: (f'--{name}', value) if value else ()
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(
|
||||
(
|
||||
'--repo',
|
||||
@@ -296,6 +320,9 @@ def test_display_repository_info_with_exit_codes_calls_borg_using_them():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_flags').replace_with(
|
||||
lambda name, value: (f'--{name}', value) if value else ()
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(
|
||||
(
|
||||
'--repo',
|
||||
@@ -335,6 +362,9 @@ def test_display_repository_info_with_remote_path_calls_borg_with_remote_path_fl
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_flags').replace_with(
|
||||
lambda name, value: (f'--{name}', value) if value else ()
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(
|
||||
(
|
||||
'--repo',
|
||||
@@ -370,10 +400,58 @@ def test_display_repository_info_with_remote_path_calls_borg_with_remote_path_fl
|
||||
)
|
||||
|
||||
|
||||
def test_display_repository_info_with_umask_calls_borg_with_umask_flags():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_flags').replace_with(
|
||||
lambda name, value: (f'--{name}', value) if value else ()
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(
|
||||
(
|
||||
'--repo',
|
||||
'repo',
|
||||
)
|
||||
)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
('borg', 'repo-info', '--umask', '077', '--json', '--repo', 'repo'),
|
||||
extra_environment=None,
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).and_return('[]')
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'repo-info', '--umask', '077', '--repo', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
extra_environment=None,
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
)
|
||||
|
||||
module.display_repository_info(
|
||||
repository_path='repo',
|
||||
config={'umask': '077'},
|
||||
local_borg_version='2.3.4',
|
||||
repo_info_arguments=flexmock(json=False),
|
||||
global_arguments=flexmock(log_json=False),
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
|
||||
def test_display_repository_info_with_log_json_calls_borg_with_log_json_flags():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_flags').replace_with(
|
||||
lambda name, value: (f'--{name}', value) if value else ()
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('log-json', True).and_return(
|
||||
('--log-json',)
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(
|
||||
(
|
||||
'--repo',
|
||||
@@ -413,6 +491,9 @@ def test_display_repository_info_with_lock_wait_calls_borg_with_lock_wait_flags(
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
config = {'lock_wait': 5}
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_flags').replace_with(
|
||||
lambda name, value: (f'--{name}', str(value)) if value else ()
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(
|
||||
(
|
||||
'--repo',
|
||||
@@ -451,6 +532,9 @@ def test_display_repository_info_calls_borg_with_working_directory():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_flags').replace_with(
|
||||
lambda name, value: (f'--{name}', value) if value else ()
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(
|
||||
(
|
||||
'--repo',
|
||||
|
||||
@@ -180,6 +180,30 @@ 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'
|
||||
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(
|
||||
('borg', 'list', '--umask', '077') + BORG_LIST_LATEST_ARGUMENTS,
|
||||
extra_environment=None,
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).and_return(expected_archive + '\n')
|
||||
|
||||
assert (
|
||||
module.resolve_archive_name(
|
||||
'repo',
|
||||
'latest',
|
||||
config={'umask': '077'},
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(log_json=False),
|
||||
)
|
||||
== expected_archive
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_archive_name_without_archives_raises():
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -455,9 +479,9 @@ def test_make_repo_list_command_includes_local_path():
|
||||
|
||||
|
||||
def test_make_repo_list_command_includes_remote_path():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(
|
||||
('--remote-path', 'borg2')
|
||||
).and_return(()).and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags').replace_with(
|
||||
lambda name, value: (f'--{name}', value) if value else ()
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
|
||||
None, None, '1.2.3'
|
||||
).and_return(())
|
||||
@@ -478,6 +502,29 @@ def test_make_repo_list_command_includes_remote_path():
|
||||
assert command == ('borg', 'list', '--remote-path', 'borg2', 'repo')
|
||||
|
||||
|
||||
def test_make_repo_list_command_includes_umask():
|
||||
flexmock(module.flags).should_receive('make_flags').replace_with(
|
||||
lambda name, value: (f'--{name}', value) if value else ()
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
|
||||
None, None, '1.2.3'
|
||||
).and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
|
||||
command = module.make_repo_list_command(
|
||||
repository_path='repo',
|
||||
config={'umask': '077'},
|
||||
local_borg_version='1.2.3',
|
||||
repo_list_arguments=flexmock(
|
||||
archive=None, paths=None, json=False, prefix=None, match_archives=None
|
||||
),
|
||||
global_arguments=flexmock(log_json=False),
|
||||
)
|
||||
|
||||
assert command == ('borg', 'list', '--umask', '077', 'repo')
|
||||
|
||||
|
||||
def test_make_repo_list_command_transforms_prefix_into_match_archives():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(()).and_return(
|
||||
('--match-archives', 'sh:foo*')
|
||||
|
||||
@@ -334,6 +334,39 @@ def test_transfer_archives_with_remote_path_calls_borg_with_remote_path_flags():
|
||||
)
|
||||
|
||||
|
||||
def test_transfer_archives_with_umask_calls_borg_with_umask_flags():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module.flags).should_receive('make_flags').replace_with(
|
||||
lambda name, value: (f'--{name}', value) if value else ()
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'transfer', '--umask', '077', '--repo', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
output_file=None,
|
||||
extra_environment=None,
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
)
|
||||
|
||||
module.transfer_archives(
|
||||
dry_run=False,
|
||||
repository_path='repo',
|
||||
config={'umask': '077'},
|
||||
local_borg_version='2.3.4',
|
||||
transfer_arguments=flexmock(
|
||||
archive=None, progress=None, match_archives=None, source_repository=None
|
||||
),
|
||||
global_arguments=flexmock(log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_transfer_archives_with_log_json_calls_borg_with_log_json_flags():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
|
||||
@@ -1397,7 +1397,6 @@ def test_collect_configuration_run_summary_executes_hooks_for_create():
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_info_for_success_with_extract():
|
||||
flexmock(module.validate).should_receive('guard_single_repository_selected')
|
||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
||||
flexmock(module).should_receive('run_configuration').and_return([])
|
||||
arguments = {'extract': flexmock(repository='repo')}
|
||||
@@ -1429,7 +1428,6 @@ def test_collect_configuration_run_summary_logs_extract_with_repository_error():
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_info_for_success_with_mount():
|
||||
flexmock(module.validate).should_receive('guard_single_repository_selected')
|
||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
||||
flexmock(module).should_receive('run_configuration').and_return([])
|
||||
arguments = {'mount': flexmock(repository='repo')}
|
||||
|
||||
@@ -103,6 +103,26 @@ def test_schema_to_sample_configuration_generates_config_sequence_of_maps_with_e
|
||||
assert config == [OrderedDict([('field1', 'Example 1'), ('field2', 'Example 2')])]
|
||||
|
||||
|
||||
def test_schema_to_sample_configuration_generates_config_sequence_of_maps_with_multiple_types():
|
||||
schema = {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': ['object', 'null'],
|
||||
'properties': OrderedDict(
|
||||
[('field1', {'example': 'Example 1'}), ('field2', {'example': 'Example 2'})]
|
||||
),
|
||||
},
|
||||
}
|
||||
flexmock(module).should_receive('get_properties').and_return(schema['items']['properties'])
|
||||
flexmock(module.ruamel.yaml.comments).should_receive('CommentedSeq').replace_with(list)
|
||||
flexmock(module).should_receive('add_comments_to_configuration_sequence')
|
||||
flexmock(module).should_receive('add_comments_to_configuration_object')
|
||||
|
||||
config = module.schema_to_sample_configuration(schema)
|
||||
|
||||
assert config == [OrderedDict([('field1', 'Example 1'), ('field2', 'Example 2')])]
|
||||
|
||||
|
||||
def test_schema_to_sample_configuration_with_unsupported_schema_raises():
|
||||
schema = {'gobbledygook': [{'type': 'not-your'}]}
|
||||
|
||||
|
||||
@@ -136,6 +136,11 @@ def test_normalize_sections_with_only_scalar_raises():
|
||||
{'exclude_if_present': ['.nobackup']},
|
||||
False,
|
||||
),
|
||||
(
|
||||
{'store_config_files': False},
|
||||
{'bootstrap': {'store_config_files': False}},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'source_directories': ['foo', 'bar']},
|
||||
{'source_directories': ['foo', 'bar']},
|
||||
@@ -259,6 +264,7 @@ def test_normalize_applies_hard_coded_normalization_to_config(
|
||||
flexmock(module).should_receive('normalize_sections').and_return([])
|
||||
|
||||
logs = module.normalize('test.yaml', config)
|
||||
expected_config.setdefault('bootstrap', {})
|
||||
|
||||
assert config == expected_config
|
||||
|
||||
|
||||
+227
-22
@@ -1,3 +1,4 @@
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.config import paths as module
|
||||
@@ -21,6 +22,10 @@ def test_expand_user_in_path_handles_none_directory():
|
||||
assert module.expand_user_in_path(None) is None
|
||||
|
||||
|
||||
def test_expand_user_in_path_handles_incorrectly_typed_directory():
|
||||
assert module.expand_user_in_path(3) is None
|
||||
|
||||
|
||||
def test_get_borgmatic_source_directory_uses_config_option():
|
||||
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
|
||||
|
||||
@@ -33,36 +38,228 @@ def test_get_borgmatic_source_directory_without_config_option_uses_default():
|
||||
assert module.get_borgmatic_source_directory({}) == '~/.borgmatic'
|
||||
|
||||
|
||||
def test_get_borgmatic_runtime_directory_uses_config_option():
|
||||
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
|
||||
|
||||
def test_replace_temporary_subdirectory_with_glob_transforms_path():
|
||||
assert (
|
||||
module.get_borgmatic_runtime_directory(
|
||||
{'user_runtime_directory': '/tmp', 'borgmatic_source_directory': '/nope'}
|
||||
)
|
||||
== '/tmp/./borgmatic'
|
||||
module.replace_temporary_subdirectory_with_glob('/tmp/borgmatic-aet8kn93/borgmatic')
|
||||
== '/tmp/borgmatic-*/borgmatic'
|
||||
)
|
||||
|
||||
|
||||
def test_get_borgmatic_runtime_directory_falls_back_to_environment_variable():
|
||||
def test_runtime_directory_uses_config_option():
|
||||
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
|
||||
flexmock(module.os.environ).should_receive('get').with_args(
|
||||
'XDG_RUNTIME_DIR', object
|
||||
).and_return('/tmp')
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
config = {'user_runtime_directory': '/run', 'borgmatic_source_directory': '/nope'}
|
||||
|
||||
assert module.get_borgmatic_runtime_directory({}) == '/tmp/./borgmatic'
|
||||
with module.Runtime_directory(config, 'prefix') as borgmatic_runtime_directory:
|
||||
assert borgmatic_runtime_directory == '/run/./borgmatic'
|
||||
|
||||
|
||||
def test_get_borgmatic_runtime_directory_defaults_to_hard_coded_path():
|
||||
def test_runtime_directory_uses_config_option_without_adding_duplicate_borgmatic_subdirectory():
|
||||
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
|
||||
flexmock(module.os.environ).should_receive('get').and_return('/run/user/0')
|
||||
flexmock(module.os).should_receive('getuid').and_return(0)
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
config = {'user_runtime_directory': '/run/borgmatic', 'borgmatic_source_directory': '/nope'}
|
||||
|
||||
assert module.get_borgmatic_runtime_directory({}) == '/run/user/0/./borgmatic'
|
||||
with module.Runtime_directory(config, 'prefix') as borgmatic_runtime_directory:
|
||||
assert borgmatic_runtime_directory == '/run/./borgmatic'
|
||||
|
||||
|
||||
def test_runtime_directory_with_relative_config_option_errors():
|
||||
flexmock(module.os).should_receive('makedirs').never()
|
||||
config = {'user_runtime_directory': 'run', 'borgmatic_source_directory': '/nope'}
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with module.Runtime_directory(
|
||||
config, 'prefix'
|
||||
) as borgmatic_runtime_directory: # noqa: F841
|
||||
pass
|
||||
|
||||
|
||||
def test_runtime_directory_falls_back_to_xdg_runtime_dir():
|
||||
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(
|
||||
'/run'
|
||||
)
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
|
||||
with module.Runtime_directory({}, 'prefix') as borgmatic_runtime_directory:
|
||||
assert borgmatic_runtime_directory == '/run/./borgmatic'
|
||||
|
||||
|
||||
def test_runtime_directory_falls_back_to_xdg_runtime_dir_without_adding_duplicate_borgmatic_subdirectory():
|
||||
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(
|
||||
'/run/borgmatic'
|
||||
)
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
|
||||
with module.Runtime_directory({}, 'prefix') as borgmatic_runtime_directory:
|
||||
assert borgmatic_runtime_directory == '/run/./borgmatic'
|
||||
|
||||
|
||||
def test_runtime_directory_with_relative_xdg_runtime_dir_errors():
|
||||
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return('run')
|
||||
flexmock(module.os).should_receive('makedirs').never()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with module.Runtime_directory({}, 'prefix') as borgmatic_runtime_directory: # noqa: F841
|
||||
pass
|
||||
|
||||
|
||||
def test_runtime_directory_falls_back_to_runtime_directory():
|
||||
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
|
||||
'/run'
|
||||
)
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
|
||||
with module.Runtime_directory({}, 'prefix') as borgmatic_runtime_directory:
|
||||
assert borgmatic_runtime_directory == '/run/./borgmatic'
|
||||
|
||||
|
||||
def test_runtime_directory_falls_back_to_runtime_directory_without_adding_duplicate_borgmatic_subdirectory():
|
||||
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
|
||||
'/run/borgmatic'
|
||||
)
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
|
||||
with module.Runtime_directory({}, 'prefix') as borgmatic_runtime_directory:
|
||||
assert borgmatic_runtime_directory == '/run/./borgmatic'
|
||||
|
||||
|
||||
def test_runtime_directory_with_relative_runtime_directory_errors():
|
||||
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
|
||||
'run'
|
||||
)
|
||||
flexmock(module.os).should_receive('makedirs').never()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with module.Runtime_directory({}, 'prefix') as borgmatic_runtime_directory: # noqa: F841
|
||||
pass
|
||||
|
||||
|
||||
def test_runtime_directory_falls_back_to_tmpdir_and_adds_temporary_subdirectory_that_get_cleaned_up():
|
||||
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
|
||||
None
|
||||
)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return('/run')
|
||||
temporary_directory = flexmock(name='/run/borgmatic-1234')
|
||||
temporary_directory.should_receive('cleanup').once()
|
||||
flexmock(module.tempfile).should_receive('TemporaryDirectory').with_args(
|
||||
prefix='borgmatic-', dir='/run'
|
||||
).and_return(temporary_directory)
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
|
||||
with module.Runtime_directory({}, 'prefix') as borgmatic_runtime_directory:
|
||||
assert borgmatic_runtime_directory == '/run/borgmatic-1234/./borgmatic'
|
||||
|
||||
|
||||
def test_runtime_directory_with_relative_tmpdir_errors():
|
||||
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
|
||||
None
|
||||
)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return('run')
|
||||
flexmock(module.tempfile).should_receive('TemporaryDirectory').never()
|
||||
flexmock(module.os).should_receive('makedirs').never()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with module.Runtime_directory({}, 'prefix') as borgmatic_runtime_directory: # noqa: F841
|
||||
pass
|
||||
|
||||
|
||||
def test_runtime_directory_falls_back_to_temp_and_adds_temporary_subdirectory_that_get_cleaned_up():
|
||||
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
|
||||
None
|
||||
)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('TEMP').and_return('/run')
|
||||
temporary_directory = flexmock(name='/run/borgmatic-1234')
|
||||
temporary_directory.should_receive('cleanup').once()
|
||||
flexmock(module.tempfile).should_receive('TemporaryDirectory').with_args(
|
||||
prefix='borgmatic-', dir='/run'
|
||||
).and_return(temporary_directory)
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
|
||||
with module.Runtime_directory({}, 'prefix') as borgmatic_runtime_directory:
|
||||
assert borgmatic_runtime_directory == '/run/borgmatic-1234/./borgmatic'
|
||||
|
||||
|
||||
def test_runtime_directory_with_relative_temp_errors():
|
||||
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
|
||||
None
|
||||
)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('TEMP').and_return('run')
|
||||
flexmock(module.tempfile).should_receive('TemporaryDirectory').never()
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with module.Runtime_directory({}, 'prefix') as borgmatic_runtime_directory: # noqa: F841
|
||||
pass
|
||||
|
||||
|
||||
def test_runtime_directory_falls_back_to_hard_coded_tmp_path_and_adds_temporary_subdirectory_that_get_cleaned_up():
|
||||
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
|
||||
None
|
||||
)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('TEMP').and_return(None)
|
||||
temporary_directory = flexmock(name='/tmp/borgmatic-1234')
|
||||
temporary_directory.should_receive('cleanup').once()
|
||||
flexmock(module.tempfile).should_receive('TemporaryDirectory').with_args(
|
||||
prefix='borgmatic-', dir='/tmp'
|
||||
).and_return(temporary_directory)
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
|
||||
with module.Runtime_directory({}, 'prefix') as borgmatic_runtime_directory:
|
||||
assert borgmatic_runtime_directory == '/tmp/borgmatic-1234/./borgmatic'
|
||||
|
||||
|
||||
def test_runtime_directory_with_erroring_cleanup_does_not_raise():
|
||||
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
|
||||
None
|
||||
)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('TEMP').and_return(None)
|
||||
temporary_directory = flexmock(name='/tmp/borgmatic-1234')
|
||||
temporary_directory.should_receive('cleanup').and_raise(OSError).once()
|
||||
flexmock(module.tempfile).should_receive('TemporaryDirectory').with_args(
|
||||
prefix='borgmatic-', dir='/tmp'
|
||||
).and_return(temporary_directory)
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
|
||||
with module.Runtime_directory({}, 'prefix') as borgmatic_runtime_directory:
|
||||
assert borgmatic_runtime_directory == '/tmp/borgmatic-1234/./borgmatic'
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'borgmatic_runtime_directory,expected_glob',
|
||||
(
|
||||
('/foo/bar/baz/./borgmatic', 'foo/bar/baz/borgmatic'),
|
||||
('/foo/borgmatic/baz/./borgmatic', 'foo/borgmatic/baz/borgmatic'),
|
||||
('/foo/borgmatic-jti8idds/./borgmatic', 'foo/*/borgmatic'),
|
||||
),
|
||||
)
|
||||
def test_make_runtime_directory_glob(borgmatic_runtime_directory, expected_glob):
|
||||
assert module.make_runtime_directory_glob(borgmatic_runtime_directory) == expected_glob
|
||||
|
||||
|
||||
def test_get_borgmatic_state_directory_uses_config_option():
|
||||
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
|
||||
flexmock(module.os.environ).should_receive('get').never()
|
||||
|
||||
assert (
|
||||
module.get_borgmatic_state_directory(
|
||||
@@ -72,16 +269,24 @@ def test_get_borgmatic_state_directory_uses_config_option():
|
||||
)
|
||||
|
||||
|
||||
def test_get_borgmatic_state_directory_falls_back_to_environment_variable():
|
||||
def test_get_borgmatic_state_directory_falls_back_to_xdg_state_home():
|
||||
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
|
||||
flexmock(module.os.environ).should_receive('get').with_args(
|
||||
'XDG_STATE_HOME', object
|
||||
).and_return('/tmp')
|
||||
flexmock(module.os.environ).should_receive('get').with_args('XDG_STATE_HOME').and_return('/tmp')
|
||||
|
||||
assert module.get_borgmatic_state_directory({}) == '/tmp/borgmatic'
|
||||
|
||||
|
||||
def test_get_borgmatic_state_directory_falls_back_to_state_directory():
|
||||
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('XDG_STATE_HOME').and_return(None)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('STATE_DIRECTORY').and_return(
|
||||
'/tmp'
|
||||
)
|
||||
|
||||
assert module.get_borgmatic_state_directory({}) == '/tmp/borgmatic'
|
||||
|
||||
|
||||
def test_get_borgmatic_state_directory_defaults_to_hard_coded_path():
|
||||
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
|
||||
flexmock(module.os.environ).should_receive('get').and_return('/root/.local/state')
|
||||
assert module.get_borgmatic_state_directory({}) == '/root/.local/state/borgmatic'
|
||||
flexmock(module.os.environ).should_receive('get').and_return(None)
|
||||
assert module.get_borgmatic_state_directory({}) == '~/.local/state/borgmatic'
|
||||
|
||||
@@ -210,32 +210,3 @@ def test_guard_configuration_contains_repository_errors_when_repository_does_not
|
||||
repository='nope',
|
||||
configurations={'config.yaml': {'repositories': ['repo', 'repo2']}},
|
||||
)
|
||||
|
||||
|
||||
def test_guard_single_repository_selected_raises_when_multiple_repositories_configured_and_none_selected():
|
||||
with pytest.raises(ValueError):
|
||||
module.guard_single_repository_selected(
|
||||
repository=None,
|
||||
configurations={'config.yaml': {'repositories': ['repo', 'repo2']}},
|
||||
)
|
||||
|
||||
|
||||
def test_guard_single_repository_selected_does_not_raise_when_single_repository_configured_and_none_selected():
|
||||
module.guard_single_repository_selected(
|
||||
repository=None,
|
||||
configurations={'config.yaml': {'repositories': ['repo']}},
|
||||
)
|
||||
|
||||
|
||||
def test_guard_single_repository_selected_does_not_raise_when_no_repositories_configured_and_one_selected():
|
||||
module.guard_single_repository_selected(
|
||||
repository='repo',
|
||||
configurations={'config.yaml': {'repositories': []}},
|
||||
)
|
||||
|
||||
|
||||
def test_guard_single_repository_selected_does_not_raise_when_repositories_configured_and_one_selected():
|
||||
module.guard_single_repository_selected(
|
||||
repository='repo',
|
||||
configurations={'config.yaml': {'repositories': ['repo', 'repo2']}},
|
||||
)
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import sys
|
||||
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.hooks import bootstrap as module
|
||||
|
||||
|
||||
def test_dump_data_sources_creates_manifest_file():
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
|
||||
flexmock(module.importlib.metadata).should_receive('version').and_return('1.0.0')
|
||||
manifest_file = flexmock(
|
||||
__enter__=lambda *args: flexmock(write=lambda *args: None, close=lambda *args: None),
|
||||
__exit__=lambda *args: None,
|
||||
)
|
||||
flexmock(sys.modules['builtins']).should_receive('open').with_args(
|
||||
'/run/borgmatic/bootstrap/manifest.json', 'w'
|
||||
).and_return(manifest_file)
|
||||
flexmock(module.json).should_receive('dump').with_args(
|
||||
{'borgmatic_version': '1.0.0', 'config_paths': ('test.yaml',)},
|
||||
manifest_file,
|
||||
).once()
|
||||
|
||||
module.dump_data_sources(
|
||||
hook_config={},
|
||||
config={},
|
||||
log_prefix='test',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_with_store_config_files_false_does_not_create_manifest_file():
|
||||
flexmock(module.os).should_receive('makedirs').never()
|
||||
flexmock(module.json).should_receive('dump').never()
|
||||
hook_config = {'store_config_files': False}
|
||||
|
||||
module.dump_data_sources(
|
||||
hook_config=hook_config,
|
||||
config={'bootstrap': hook_config},
|
||||
log_prefix='test',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=True,
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_with_dry_run_does_not_create_manifest_file():
|
||||
flexmock(module.os).should_receive('makedirs').never()
|
||||
flexmock(module.json).should_receive('dump').never()
|
||||
|
||||
module.dump_data_sources(
|
||||
hook_config={},
|
||||
config={},
|
||||
log_prefix='test',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=True,
|
||||
)
|
||||
|
||||
|
||||
def test_remove_data_source_dumps_deletes_manifest_and_parent_directory():
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'replace_temporary_subdirectory_with_glob'
|
||||
).and_return('/run/borgmatic')
|
||||
flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
|
||||
flexmock(module.os).should_receive('remove').with_args(
|
||||
'/run/borgmatic/bootstrap/manifest.json'
|
||||
).once()
|
||||
flexmock(module.os).should_receive('rmdir').with_args('/run/borgmatic/bootstrap').once()
|
||||
|
||||
module.remove_data_source_dumps(
|
||||
hook_config={},
|
||||
config={},
|
||||
log_prefix='test',
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_remove_data_source_dumps_with_dry_run_bails():
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'replace_temporary_subdirectory_with_glob'
|
||||
).and_return('/run/borgmatic')
|
||||
flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
|
||||
flexmock(module.os).should_receive('remove').never()
|
||||
flexmock(module.os).should_receive('rmdir').never()
|
||||
|
||||
module.remove_data_source_dumps(
|
||||
hook_config={},
|
||||
config={},
|
||||
log_prefix='test',
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
dry_run=True,
|
||||
)
|
||||
|
||||
|
||||
def test_remove_data_source_dumps_swallows_manifest_file_not_found_error():
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'replace_temporary_subdirectory_with_glob'
|
||||
).and_return('/run/borgmatic')
|
||||
flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
|
||||
flexmock(module.os).should_receive('remove').with_args(
|
||||
'/run/borgmatic/bootstrap/manifest.json'
|
||||
).and_raise(FileNotFoundError).once()
|
||||
flexmock(module.os).should_receive('rmdir').with_args('/run/borgmatic/bootstrap').once()
|
||||
|
||||
module.remove_data_source_dumps(
|
||||
hook_config={},
|
||||
config={},
|
||||
log_prefix='test',
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_remove_data_source_dumps_swallows_manifest_parent_directory_not_found_error():
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'replace_temporary_subdirectory_with_glob'
|
||||
).and_return('/run/borgmatic')
|
||||
flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
|
||||
flexmock(module.os).should_receive('remove').with_args(
|
||||
'/run/borgmatic/bootstrap/manifest.json'
|
||||
).once()
|
||||
flexmock(module.os).should_receive('rmdir').with_args('/run/borgmatic/bootstrap').and_raise(
|
||||
FileNotFoundError
|
||||
).once()
|
||||
|
||||
module.remove_data_source_dumps(
|
||||
hook_config={},
|
||||
config={},
|
||||
log_prefix='test',
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
dry_run=False,
|
||||
)
|
||||
@@ -77,10 +77,12 @@ def test_call_hooks_calls_skips_return_values_for_missing_hooks():
|
||||
assert return_values == expected_return_values
|
||||
|
||||
|
||||
def test_call_hooks_calls_skips_return_values_for_null_hooks():
|
||||
def test_call_hooks_calls_treats_null_hook_as_optionless():
|
||||
config = {'super_hook': flexmock(), 'other_hook': None}
|
||||
expected_return_values = {'super_hook': flexmock()}
|
||||
flexmock(module).should_receive('call_hook').and_return(expected_return_values['super_hook'])
|
||||
expected_return_values = {'super_hook': flexmock(), 'other_hook': flexmock()}
|
||||
flexmock(module).should_receive('call_hook').and_return(
|
||||
expected_return_values['super_hook']
|
||||
).and_return(expected_return_values['other_hook'])
|
||||
|
||||
return_values = module.call_hooks(
|
||||
'do_stuff', config, 'prefix', ('super_hook', 'other_hook'), 55
|
||||
|
||||
@@ -73,7 +73,18 @@ def test_dump_data_sources_dumps_each_database():
|
||||
dry_run_label=object,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
)
|
||||
== processes
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_dumps_with_password():
|
||||
@@ -94,7 +105,15 @@ def test_dump_data_sources_dumps_with_password():
|
||||
dry_run_label=object,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources([database], {}, 'test.yaml', dry_run=False) == [process]
|
||||
assert module.dump_data_sources(
|
||||
[database],
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
) == [process]
|
||||
|
||||
|
||||
def test_dump_data_sources_dumps_all_databases_at_once():
|
||||
@@ -112,7 +131,15 @@ def test_dump_data_sources_dumps_all_databases_at_once():
|
||||
dry_run_label=object,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
|
||||
assert module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
) == [process]
|
||||
|
||||
|
||||
def test_dump_data_sources_dumps_all_databases_separately_when_format_configured():
|
||||
@@ -132,7 +159,18 @@ def test_dump_data_sources_dumps_all_databases_separately_when_format_configured
|
||||
dry_run_label=object,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
)
|
||||
== processes
|
||||
)
|
||||
|
||||
|
||||
def test_database_names_to_dump_runs_mariadb_with_list_options():
|
||||
@@ -434,7 +472,15 @@ def test_dump_data_sources_errors_for_missing_all_databases():
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False)
|
||||
assert module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run():
|
||||
@@ -445,7 +491,18 @@ def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run
|
||||
)
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=True) == []
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=True,
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
|
||||
def test_restore_data_source_dump_runs_mariadb_to_restore():
|
||||
@@ -473,6 +530,7 @@ def test_restore_data_source_dump_runs_mariadb_to_restore():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -501,6 +559,7 @@ def test_restore_data_source_dump_runs_mariadb_with_options():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -531,6 +590,7 @@ def test_restore_data_source_dump_runs_non_default_mariadb_with_options():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -568,6 +628,7 @@ def test_restore_data_source_dump_runs_mariadb_with_hostname_and_port():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -596,6 +657,7 @@ def test_restore_data_source_dump_runs_mariadb_with_username_and_password():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -645,6 +707,7 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
|
||||
'username': 'cliusername',
|
||||
'password': 'clipassword',
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -696,6 +759,7 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -717,4 +781,5 @@ def test_restore_data_source_dump_with_dry_run_skips_restore():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
@@ -41,7 +41,18 @@ def test_dump_data_sources_runs_mongodump_for_each_database():
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
)
|
||||
== processes
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_with_dry_run_skips_mongodump():
|
||||
@@ -53,7 +64,18 @@ def test_dump_data_sources_with_dry_run_skips_mongodump():
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=True) == []
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=True,
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_runs_mongodump_with_hostname_and_port():
|
||||
@@ -82,7 +104,15 @@ def test_dump_data_sources_runs_mongodump_with_hostname_and_port():
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
|
||||
assert module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
) == [process]
|
||||
|
||||
|
||||
def test_dump_data_sources_runs_mongodump_with_username_and_password():
|
||||
@@ -120,7 +150,15 @@ def test_dump_data_sources_runs_mongodump_with_username_and_password():
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
|
||||
assert module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
) == [process]
|
||||
|
||||
|
||||
def test_dump_data_sources_runs_mongodump_with_directory_format():
|
||||
@@ -137,7 +175,18 @@ def test_dump_data_sources_runs_mongodump_with_directory_format():
|
||||
shell=True,
|
||||
).and_return(flexmock()).once()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == []
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_runs_mongodump_with_options():
|
||||
@@ -163,7 +212,15 @@ def test_dump_data_sources_runs_mongodump_with_options():
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
|
||||
assert module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
) == [process]
|
||||
|
||||
|
||||
def test_dump_data_sources_runs_mongodumpall_for_all_databases():
|
||||
@@ -181,7 +238,15 @@ def test_dump_data_sources_runs_mongodumpall_for_all_databases():
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
|
||||
assert module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
) == [process]
|
||||
|
||||
|
||||
def test_build_dump_command_with_username_injection_attack_gets_escaped():
|
||||
@@ -218,6 +283,7 @@ def test_restore_data_source_dump_runs_mongorestore():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -257,6 +323,7 @@ def test_restore_data_source_dump_runs_mongorestore_with_hostname_and_port():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -304,6 +371,7 @@ def test_restore_data_source_dump_runs_mongorestore_with_username_and_password()
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -359,6 +427,7 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
|
||||
'username': 'cliusername',
|
||||
'password': 'clipassword',
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -414,6 +483,7 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -443,6 +513,7 @@ def test_restore_data_source_dump_runs_mongorestore_with_options():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -480,6 +551,7 @@ def test_restore_databases_dump_runs_mongorestore_with_schemas():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -509,6 +581,7 @@ def test_restore_data_source_dump_runs_psql_for_all_database_dump():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -532,6 +605,7 @@ def test_restore_data_source_dump_with_dry_run_skips_restore():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -560,4 +634,5 @@ def test_restore_data_source_dump_without_extract_process_restores_from_disk():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
@@ -73,7 +73,18 @@ def test_dump_data_sources_dumps_each_database():
|
||||
dry_run_label=object,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
)
|
||||
== processes
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_dumps_with_password():
|
||||
@@ -94,7 +105,15 @@ def test_dump_data_sources_dumps_with_password():
|
||||
dry_run_label=object,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources([database], {}, 'test.yaml', dry_run=False) == [process]
|
||||
assert module.dump_data_sources(
|
||||
[database],
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
) == [process]
|
||||
|
||||
|
||||
def test_dump_data_sources_dumps_all_databases_at_once():
|
||||
@@ -112,7 +131,15 @@ def test_dump_data_sources_dumps_all_databases_at_once():
|
||||
dry_run_label=object,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
|
||||
assert module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
) == [process]
|
||||
|
||||
|
||||
def test_dump_data_sources_dumps_all_databases_separately_when_format_configured():
|
||||
@@ -132,7 +159,18 @@ def test_dump_data_sources_dumps_all_databases_separately_when_format_configured
|
||||
dry_run_label=object,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
)
|
||||
== processes
|
||||
)
|
||||
|
||||
|
||||
def test_database_names_to_dump_runs_mysql_with_list_options():
|
||||
@@ -432,7 +470,15 @@ def test_dump_data_sources_errors_for_missing_all_databases():
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False)
|
||||
assert module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run():
|
||||
@@ -443,7 +489,18 @@ def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run
|
||||
)
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=True) == []
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=True,
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
|
||||
def test_restore_data_source_dump_runs_mysql_to_restore():
|
||||
@@ -471,6 +528,7 @@ def test_restore_data_source_dump_runs_mysql_to_restore():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -499,6 +557,7 @@ def test_restore_data_source_dump_runs_mysql_with_options():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -527,6 +586,7 @@ def test_restore_data_source_dump_runs_non_default_mysql_with_options():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -564,6 +624,7 @@ def test_restore_data_source_dump_runs_mysql_with_hostname_and_port():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -592,6 +653,7 @@ def test_restore_data_source_dump_runs_mysql_with_username_and_password():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -641,6 +703,7 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
|
||||
'username': 'cliusername',
|
||||
'password': 'clipassword',
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -692,6 +755,7 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -713,4 +777,5 @@ def test_restore_data_source_dump_with_dry_run_skips_restore():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
@@ -251,7 +251,18 @@ def test_dump_data_sources_runs_pg_dump_for_each_database():
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
)
|
||||
== processes
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_raises_when_no_database_names_to_dump():
|
||||
@@ -261,7 +272,15 @@ def test_dump_data_sources_raises_when_no_database_names_to_dump():
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False)
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_does_not_raise_when_no_database_names_to_dump():
|
||||
@@ -270,7 +289,15 @@ def test_dump_data_sources_does_not_raise_when_no_database_names_to_dump():
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
||||
|
||||
module.dump_data_sources(databases, {}, 'test.yaml', dry_run=True) == []
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=True,
|
||||
) == []
|
||||
|
||||
|
||||
def test_dump_data_sources_with_duplicate_dump_skips_pg_dump():
|
||||
@@ -287,7 +314,18 @@ def test_dump_data_sources_with_duplicate_dump_skips_pg_dump():
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == []
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_with_dry_run_skips_pg_dump():
|
||||
@@ -304,7 +342,18 @@ def test_dump_data_sources_with_dry_run_skips_pg_dump():
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=True) == []
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=True,
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_runs_pg_dump_with_hostname_and_port():
|
||||
@@ -340,7 +389,15 @@ def test_dump_data_sources_runs_pg_dump_with_hostname_and_port():
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
|
||||
assert module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
) == [process]
|
||||
|
||||
|
||||
def test_dump_data_sources_runs_pg_dump_with_username_and_password():
|
||||
@@ -376,7 +433,15 @@ def test_dump_data_sources_runs_pg_dump_with_username_and_password():
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
|
||||
assert module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
) == [process]
|
||||
|
||||
|
||||
def test_dump_data_sources_with_username_injection_attack_gets_escaped():
|
||||
@@ -412,7 +477,15 @@ def test_dump_data_sources_with_username_injection_attack_gets_escaped():
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
|
||||
assert module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
) == [process]
|
||||
|
||||
|
||||
def test_dump_data_sources_runs_pg_dump_with_directory_format():
|
||||
@@ -443,7 +516,18 @@ def test_dump_data_sources_runs_pg_dump_with_directory_format():
|
||||
extra_environment={'PGSSLMODE': 'disable'},
|
||||
).and_return(flexmock()).once()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == []
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_runs_pg_dump_with_options():
|
||||
@@ -476,7 +560,15 @@ def test_dump_data_sources_runs_pg_dump_with_options():
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
|
||||
assert module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
) == [process]
|
||||
|
||||
|
||||
def test_dump_data_sources_runs_pg_dumpall_for_all_databases():
|
||||
@@ -498,7 +590,15 @@ def test_dump_data_sources_runs_pg_dumpall_for_all_databases():
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
|
||||
assert module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
) == [process]
|
||||
|
||||
|
||||
def test_dump_data_sources_runs_non_default_pg_dump():
|
||||
@@ -532,7 +632,15 @@ def test_dump_data_sources_runs_non_default_pg_dump():
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
|
||||
assert module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
) == [process]
|
||||
|
||||
|
||||
def test_restore_data_source_dump_runs_pg_restore():
|
||||
@@ -584,6 +692,7 @@ def test_restore_data_source_dump_runs_pg_restore():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -646,6 +755,7 @@ def test_restore_data_source_dump_runs_pg_restore_with_hostname_and_port():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -706,6 +816,7 @@ def test_restore_data_source_dump_runs_pg_restore_with_username_and_password():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -785,6 +896,7 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
|
||||
'username': 'cliusername',
|
||||
'password': 'clipassword',
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -864,6 +976,7 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -925,6 +1038,7 @@ def test_restore_data_source_dump_runs_pg_restore_with_options():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -964,6 +1078,7 @@ def test_restore_data_source_dump_runs_psql_for_all_database_dump():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -1008,6 +1123,7 @@ def test_restore_data_source_dump_runs_psql_for_plain_database_dump():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -1077,6 +1193,7 @@ def test_restore_data_source_dump_runs_non_default_pg_restore_and_psql():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -1101,6 +1218,7 @@ def test_restore_data_source_dump_with_dry_run_skips_restore():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -1153,6 +1271,7 @@ def test_restore_data_source_dump_without_extract_process_restores_from_disk():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -1209,4 +1328,5 @@ def test_restore_data_source_dump_with_schemas_restores_schemas():
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
@@ -0,0 +1,522 @@
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
import borgmatic.hooks.monitor
|
||||
from borgmatic.hooks import pushover as module
|
||||
|
||||
|
||||
def test_ping_monitor_config_with_minimum_config_fail_state_backup_successfully_send_to_pushover():
|
||||
'''
|
||||
This test should be the minimum working configuration. The "message"
|
||||
should be auto populated with the default value which is the state name.
|
||||
'''
|
||||
hook_config = {'token': 'ksdjfwoweijfvwoeifvjmwghagy92', 'user': '983hfe0of902lkjfa2amanfgui'}
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'https://api.pushover.net/1/messages.json',
|
||||
headers={'Content-type': 'application/x-www-form-urlencoded'},
|
||||
data={
|
||||
'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
'message': 'fail',
|
||||
},
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
hook_config,
|
||||
{},
|
||||
'config.yaml',
|
||||
borgmatic.hooks.monitor.State.FAIL,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_config_with_minimum_config_start_state_backup_not_send_to_pushover_exit_early():
|
||||
'''
|
||||
This test should exit early since the hook config does not specify the
|
||||
'start' state. Only the 'fail' state is enabled by default.
|
||||
'''
|
||||
hook_config = {'token': 'ksdjfwoweijfvwoeifvjmwghagy92', 'user': '983hfe0of902lkjfa2amanfgui'}
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
flexmock(module.requests).should_receive('post').never()
|
||||
|
||||
module.ping_monitor(
|
||||
hook_config,
|
||||
{},
|
||||
'config.yaml',
|
||||
borgmatic.hooks.monitor.State.START,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_start_state_backup_default_message_successfully_send_to_pushover():
|
||||
'''
|
||||
This test should send a notification to Pushover on backup start
|
||||
since the state has been configured. It should default to sending
|
||||
the name of the state as the 'message' since it is not
|
||||
explicitly declared in the state config.
|
||||
'''
|
||||
hook_config = {
|
||||
'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
'states': {'start', 'fail', 'finish'},
|
||||
}
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'https://api.pushover.net/1/messages.json',
|
||||
headers={'Content-type': 'application/x-www-form-urlencoded'},
|
||||
data={
|
||||
'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
'message': 'start',
|
||||
},
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
hook_config,
|
||||
{},
|
||||
'config.yaml',
|
||||
borgmatic.hooks.monitor.State.START,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_start_state_backup_custom_message_successfully_send_to_pushover():
|
||||
'''
|
||||
This test should send a notification to Pushover on backup start
|
||||
since the state has been configured. It should send a custom
|
||||
'message' since it is explicitly declared in the state config.
|
||||
'''
|
||||
hook_config = {
|
||||
'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
'states': {'start', 'fail', 'finish'},
|
||||
'start': {'message': 'custom start message'},
|
||||
}
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'https://api.pushover.net/1/messages.json',
|
||||
headers={'Content-type': 'application/x-www-form-urlencoded'},
|
||||
data={
|
||||
'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
'message': 'custom start message',
|
||||
},
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
hook_config,
|
||||
{},
|
||||
'config.yaml',
|
||||
borgmatic.hooks.monitor.State.START,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_start_state_backup_default_message_with_priority_emergency_uses_expire_and_retry_defaults():
|
||||
'''
|
||||
This simulates priority level 2 being set but expiry and retry are
|
||||
not declared. This should set retry and expiry to their defaults.
|
||||
'''
|
||||
hook_config = {
|
||||
'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
'states': {'start', 'fail', 'finish'},
|
||||
'start': {'priority': 2},
|
||||
}
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'https://api.pushover.net/1/messages.json',
|
||||
headers={'Content-type': 'application/x-www-form-urlencoded'},
|
||||
data={
|
||||
'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
'message': 'start',
|
||||
'priority': 2,
|
||||
'retry': 30,
|
||||
'expire': 600,
|
||||
},
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
hook_config,
|
||||
{},
|
||||
'config.yaml',
|
||||
borgmatic.hooks.monitor.State.START,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_start_state_backup_default_message_with_priority_emergency_declared_with_expire_no_retry_success():
|
||||
'''
|
||||
This simulates priority level 2 and expiry being set but retry is
|
||||
not declared. This should set retry to the default.
|
||||
'''
|
||||
hook_config = {
|
||||
'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
'states': {'start', 'fail', 'finish'},
|
||||
'start': {'priority': 2, 'expire': 600},
|
||||
}
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'https://api.pushover.net/1/messages.json',
|
||||
headers={'Content-type': 'application/x-www-form-urlencoded'},
|
||||
data={
|
||||
'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
'message': 'start',
|
||||
'priority': 2,
|
||||
'retry': 30,
|
||||
'expire': 600,
|
||||
},
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
hook_config,
|
||||
{},
|
||||
'config.yaml',
|
||||
borgmatic.hooks.monitor.State.START,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_start_state_backup_default_message_with_priority_emergency_declared_no_expire_with_retry_success():
|
||||
'''
|
||||
This simulates priority level 2 and retry being set but expire is
|
||||
not declared. This should set expire to the default.
|
||||
'''
|
||||
hook_config = {
|
||||
'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
'states': {'start', 'fail', 'finish'},
|
||||
'start': {'priority': 2, 'retry': 30},
|
||||
}
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'https://api.pushover.net/1/messages.json',
|
||||
headers={'Content-type': 'application/x-www-form-urlencoded'},
|
||||
data={
|
||||
'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
'message': 'start',
|
||||
'priority': 2,
|
||||
'retry': 30,
|
||||
'expire': 600,
|
||||
},
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
hook_config,
|
||||
{},
|
||||
'config.yaml',
|
||||
borgmatic.hooks.monitor.State.START,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_start_state_backup_default_message_with_priority_high_declared_expire_and_retry_raises():
|
||||
'''
|
||||
This simulates priority level 1, retry and expiry being set. Since expire
|
||||
and retry are only used for priority level 2, they should not be included
|
||||
in the request sent to Pushover. This test verifies that a ValueError is
|
||||
raised.
|
||||
'''
|
||||
hook_config = {
|
||||
'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
'states': {'start', 'fail', 'finish'},
|
||||
'start': {'priority': 1, 'expire': 30, 'retry': 30},
|
||||
}
|
||||
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
flexmock(module.requests).should_receive('post').never()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.ping_monitor(
|
||||
hook_config,
|
||||
{},
|
||||
'config.yaml',
|
||||
borgmatic.hooks.monitor.State.START,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_start_state_backup_based_on_documentation_advanced_example_success():
|
||||
'''
|
||||
Here is a test of what is provided in the monitor-your-backups.md file
|
||||
as an 'advanced example'. This test runs the start state.
|
||||
'''
|
||||
hook_config = {
|
||||
'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
'states': {'start', 'fail', 'finish'},
|
||||
'start': {
|
||||
'message': 'Backup <b>Started</b>',
|
||||
'priority': -2,
|
||||
'title': 'Backup Started',
|
||||
'html': 1,
|
||||
'ttl': 10,
|
||||
},
|
||||
'fail': {
|
||||
'message': 'Backup <font color="#ff6961">Failed</font>',
|
||||
'priority': 2,
|
||||
'expire': 600,
|
||||
'retry': 30,
|
||||
'device': 'pixel8',
|
||||
'title': 'Backup Failed',
|
||||
'html': 1,
|
||||
'sound': 'siren',
|
||||
'url': 'https://ticketing-system.example.com/login',
|
||||
'url_title': 'Login to ticketing system',
|
||||
},
|
||||
'finish': {
|
||||
'message': 'Backup <font color="#77dd77">Finished</font>',
|
||||
'priority': 0,
|
||||
'title': 'Backup Finished',
|
||||
'html': 1,
|
||||
'ttl': 60,
|
||||
'url': 'https://ticketing-system.example.com/login',
|
||||
'url_title': 'Login to ticketing system',
|
||||
},
|
||||
}
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'https://api.pushover.net/1/messages.json',
|
||||
headers={'Content-type': 'application/x-www-form-urlencoded'},
|
||||
data={
|
||||
'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
'message': 'Backup <b>Started</b>',
|
||||
'priority': -2,
|
||||
'title': 'Backup Started',
|
||||
'html': 1,
|
||||
'ttl': 10,
|
||||
},
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
hook_config,
|
||||
{},
|
||||
'config.yaml',
|
||||
borgmatic.hooks.monitor.State.START,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_fail_state_backup_based_on_documentation_advanced_example_success():
|
||||
'''
|
||||
Here is a test of what is provided in the monitor-your-backups.md file
|
||||
as an 'advanced example'. This test runs the fail state.
|
||||
'''
|
||||
hook_config = {
|
||||
'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
'states': {'start', 'fail', 'finish'},
|
||||
'start': {
|
||||
'message': 'Backup <b>Started</b>',
|
||||
'priority': -2,
|
||||
'title': 'Backup Started',
|
||||
'html': 1,
|
||||
'ttl': 10,
|
||||
},
|
||||
'fail': {
|
||||
'message': 'Backup <font color="#ff6961">Failed</font>',
|
||||
'priority': 2,
|
||||
'expire': 600,
|
||||
'retry': 30,
|
||||
'device': 'pixel8',
|
||||
'title': 'Backup Failed',
|
||||
'html': 1,
|
||||
'sound': 'siren',
|
||||
'url': 'https://ticketing-system.example.com/login',
|
||||
'url_title': 'Login to ticketing system',
|
||||
},
|
||||
'finish': {
|
||||
'message': 'Backup <font color="#77dd77">Finished</font>',
|
||||
'priority': 0,
|
||||
'title': 'Backup Finished',
|
||||
'html': 1,
|
||||
'ttl': 60,
|
||||
'url': 'https://ticketing-system.example.com/login',
|
||||
'url_title': 'Login to ticketing system',
|
||||
},
|
||||
}
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'https://api.pushover.net/1/messages.json',
|
||||
headers={'Content-type': 'application/x-www-form-urlencoded'},
|
||||
data={
|
||||
'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
'message': 'Backup <font color="#ff6961">Failed</font>',
|
||||
'priority': 2,
|
||||
'expire': 600,
|
||||
'retry': 30,
|
||||
'device': 'pixel8',
|
||||
'title': 'Backup Failed',
|
||||
'html': 1,
|
||||
'sound': 'siren',
|
||||
'url': 'https://ticketing-system.example.com/login',
|
||||
'url_title': 'Login to ticketing system',
|
||||
},
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
hook_config,
|
||||
{},
|
||||
'config.yaml',
|
||||
borgmatic.hooks.monitor.State.FAIL,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_finish_state_backup_based_on_documentation_advanced_example_success():
|
||||
'''
|
||||
Here is a test of what is provided in the monitor-your-backups.md file
|
||||
as an 'advanced example'. This test runs the finish state.
|
||||
'''
|
||||
hook_config = {
|
||||
'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
'states': {'start', 'fail', 'finish'},
|
||||
'start': {
|
||||
'message': 'Backup <b>Started</b>',
|
||||
'priority': -2,
|
||||
'title': 'Backup Started',
|
||||
'html': 1,
|
||||
'ttl': 10,
|
||||
},
|
||||
'fail': {
|
||||
'message': 'Backup <font color="#ff6961">Failed</font>',
|
||||
'priority': 2,
|
||||
'expire': 600,
|
||||
'retry': 30,
|
||||
'device': 'pixel8',
|
||||
'title': 'Backup Failed',
|
||||
'html': 1,
|
||||
'sound': 'siren',
|
||||
'url': 'https://ticketing-system.example.com/login',
|
||||
'url_title': 'Login to ticketing system',
|
||||
},
|
||||
'finish': {
|
||||
'message': 'Backup <font color="#77dd77">Finished</font>',
|
||||
'priority': 0,
|
||||
'title': 'Backup Finished',
|
||||
'html': 1,
|
||||
'ttl': 60,
|
||||
'url': 'https://ticketing-system.example.com/login',
|
||||
'url_title': 'Login to ticketing system',
|
||||
},
|
||||
}
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'https://api.pushover.net/1/messages.json',
|
||||
headers={'Content-type': 'application/x-www-form-urlencoded'},
|
||||
data={
|
||||
'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
'message': 'Backup <font color="#77dd77">Finished</font>',
|
||||
'priority': 0,
|
||||
'title': 'Backup Finished',
|
||||
'html': 1,
|
||||
'ttl': 60,
|
||||
'url': 'https://ticketing-system.example.com/login',
|
||||
'url_title': 'Login to ticketing system',
|
||||
},
|
||||
).and_return(flexmock(ok=True)).once()
|
||||
|
||||
module.ping_monitor(
|
||||
hook_config,
|
||||
{},
|
||||
'config.yaml',
|
||||
borgmatic.hooks.monitor.State.FINISH,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_config_with_minimum_config_fail_state_backup_successfully_send_to_pushover_dryrun():
|
||||
'''
|
||||
This test should be the minimum working configuration. The "message"
|
||||
should be auto populated with the default value which is the state name.
|
||||
'''
|
||||
hook_config = {'token': 'ksdjfwoweijfvwoeifvjmwghagy92', 'user': '983hfe0of902lkjfa2amanfgui'}
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
flexmock(module.requests).should_receive('post').and_return(flexmock(ok=True)).never()
|
||||
|
||||
module.ping_monitor(
|
||||
hook_config,
|
||||
{},
|
||||
'config.yaml',
|
||||
borgmatic.hooks.monitor.State.FAIL,
|
||||
monitoring_log_level=1,
|
||||
dry_run=True,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_config_incorrect_state_exit_early():
|
||||
'''
|
||||
This test should exit early since the start state is not declared in the configuration.
|
||||
'''
|
||||
hook_config = {
|
||||
'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
}
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
flexmock(module.requests).should_receive('post').and_return(flexmock(ok=True)).never()
|
||||
|
||||
module.ping_monitor(
|
||||
hook_config,
|
||||
{},
|
||||
'config.yaml',
|
||||
borgmatic.hooks.monitor.State.START,
|
||||
monitoring_log_level=1,
|
||||
dry_run=True,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_push_post_error_exits_early():
|
||||
'''
|
||||
This test simulates the Pushover servers not responding with a 200 OK. We
|
||||
should raise for status and warn then exit.
|
||||
'''
|
||||
hook_config = hook_config = {
|
||||
'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
}
|
||||
|
||||
push_response = flexmock(ok=False)
|
||||
push_response.should_receive('raise_for_status').and_raise(
|
||||
module.requests.ConnectionError
|
||||
).once()
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'https://api.pushover.net/1/messages.json',
|
||||
headers={'Content-type': 'application/x-www-form-urlencoded'},
|
||||
data={
|
||||
'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
|
||||
'user': '983hfe0of902lkjfa2amanfgui',
|
||||
'message': 'fail',
|
||||
},
|
||||
).and_return(push_response).once()
|
||||
|
||||
flexmock(module.logger).should_receive('warning').once()
|
||||
|
||||
module.ping_monitor(
|
||||
hook_config,
|
||||
{},
|
||||
'config.yaml',
|
||||
borgmatic.hooks.monitor.State.FAIL,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
@@ -18,15 +18,26 @@ def test_use_streaming_false_for_no_databases():
|
||||
def test_dump_data_sources_logs_and_skips_if_dump_already_exists():
|
||||
databases = [{'path': '/path/to/database', 'name': 'database'}]
|
||||
|
||||
flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump')
|
||||
flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'/path/to/dump/database'
|
||||
'/run/borgmatic/database'
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == []
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_dumps_each_database():
|
||||
@@ -36,9 +47,9 @@ def test_dump_data_sources_dumps_each_database():
|
||||
]
|
||||
processes = [flexmock(), flexmock()]
|
||||
|
||||
flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump')
|
||||
flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'/path/to/dump/database'
|
||||
'/run/borgmatic/database'
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
||||
@@ -46,7 +57,18 @@ def test_dump_data_sources_dumps_each_database():
|
||||
processes[1]
|
||||
)
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
)
|
||||
== processes
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_with_path_injection_attack_gets_escaped():
|
||||
@@ -55,9 +77,9 @@ def test_dump_data_sources_with_path_injection_attack_gets_escaped():
|
||||
]
|
||||
processes = [flexmock()]
|
||||
|
||||
flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump')
|
||||
flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'/path/to/dump/database'
|
||||
'/run/borgmatic/database'
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
||||
@@ -67,13 +89,24 @@ def test_dump_data_sources_with_path_injection_attack_gets_escaped():
|
||||
"'/path/to/database1; naughty-command'",
|
||||
'.dump',
|
||||
'>',
|
||||
'/path/to/dump/database',
|
||||
'/run/borgmatic/database',
|
||||
),
|
||||
shell=True,
|
||||
run_to_completion=False,
|
||||
).and_return(processes[0])
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
)
|
||||
== processes
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_with_non_existent_path_warns_and_dumps_database():
|
||||
@@ -82,16 +115,27 @@ def test_dump_data_sources_with_non_existent_path_warns_and_dumps_database():
|
||||
]
|
||||
processes = [flexmock()]
|
||||
|
||||
flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump')
|
||||
flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')
|
||||
flexmock(module.logger).should_receive('warning').once()
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'/path/to/dump/database'
|
||||
'/run/borgmatic'
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
||||
flexmock(module).should_receive('execute_command').and_return(processes[0])
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
)
|
||||
== processes
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_with_name_all_warns_and_dumps_all_databases():
|
||||
@@ -100,32 +144,54 @@ def test_dump_data_sources_with_name_all_warns_and_dumps_all_databases():
|
||||
]
|
||||
processes = [flexmock()]
|
||||
|
||||
flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump')
|
||||
flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')
|
||||
flexmock(module.logger).should_receive(
|
||||
'warning'
|
||||
).twice() # once for the name=all, once for the non-existent path
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'/path/to/dump/database'
|
||||
'/run/borgmatic/database'
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
||||
flexmock(module).should_receive('execute_command').and_return(processes[0])
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=False,
|
||||
)
|
||||
== processes
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_does_not_dump_if_dry_run():
|
||||
databases = [{'path': '/path/to/database', 'name': 'database'}]
|
||||
|
||||
flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump')
|
||||
flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'/path/to/dump/database'
|
||||
'/run/borgmatic'
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=True) == []
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
'test.yaml',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=[],
|
||||
dry_run=True,
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
|
||||
def test_restore_data_source_dump_restores_database():
|
||||
@@ -152,6 +218,7 @@ def test_restore_data_source_dump_restores_database():
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={'restore_path': None},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -181,6 +248,7 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={'restore_path': 'cli/path/to/database'},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -210,6 +278,7 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={'restore_path': None},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
@@ -228,4 +297,5 @@ def test_restore_data_source_dump_does_not_restore_database_if_dry_run():
|
||||
dry_run=True,
|
||||
extract_process=extract_process,
|
||||
connection_params={'restore_path': None},
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
import borgmatic.execute
|
||||
from borgmatic.hooks import zfs as module
|
||||
|
||||
|
||||
def test_get_datasets_to_backup_filters_datasets_by_source_directories():
|
||||
flexmock(borgmatic.execute).should_receive('execute_command_and_capture_output').and_return(
|
||||
'dataset\t/dataset\t-\nother\t/other\t-',
|
||||
)
|
||||
|
||||
assert module.get_datasets_to_backup(
|
||||
'zfs', source_directories=('/foo', '/dataset', '/bar')
|
||||
) == (('dataset', '/dataset'),)
|
||||
|
||||
|
||||
def test_get_datasets_to_backup_filters_datasets_by_user_property():
|
||||
flexmock(borgmatic.execute).should_receive('execute_command_and_capture_output').and_return(
|
||||
'dataset\t/dataset\tauto\nother\t/other\t-',
|
||||
)
|
||||
|
||||
assert module.get_datasets_to_backup('zfs', source_directories=('/foo', '/bar')) == (
|
||||
('dataset', '/dataset'),
|
||||
)
|
||||
|
||||
|
||||
def test_get_datasets_to_backup_with_invalid_list_output_raises():
|
||||
flexmock(borgmatic.execute).should_receive('execute_command_and_capture_output').and_return(
|
||||
'dataset',
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match='zfs'):
|
||||
module.get_datasets_to_backup('zfs', source_directories=('/foo', '/bar'))
|
||||
|
||||
|
||||
def test_get_get_all_datasets_does_not_filter_datasets():
|
||||
flexmock(borgmatic.execute).should_receive('execute_command_and_capture_output').and_return(
|
||||
'dataset\t/dataset\nother\t/other',
|
||||
)
|
||||
|
||||
assert module.get_all_datasets('zfs') == (
|
||||
('dataset', '/dataset'),
|
||||
('other', '/other'),
|
||||
)
|
||||
|
||||
|
||||
def test_get_all_datasets_with_invalid_list_output_raises():
|
||||
flexmock(borgmatic.execute).should_receive('execute_command_and_capture_output').and_return(
|
||||
'dataset',
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match='zfs'):
|
||||
module.get_all_datasets('zfs')
|
||||
|
||||
|
||||
def test_dump_data_sources_snapshots_and_mounts_and_updates_source_directories():
|
||||
flexmock(module).should_receive('get_datasets_to_backup').and_return(
|
||||
(('dataset', '/mnt/dataset'),)
|
||||
)
|
||||
flexmock(module.os).should_receive('getpid').and_return(1234)
|
||||
full_snapshot_name = 'dataset@borgmatic-1234'
|
||||
flexmock(module).should_receive('snapshot_dataset').with_args(
|
||||
'zfs',
|
||||
full_snapshot_name,
|
||||
).once()
|
||||
snapshot_mount_path = '/run/borgmatic/zfs_snapshots/./mnt/dataset'
|
||||
flexmock(module).should_receive('mount_snapshot').with_args(
|
||||
'mount',
|
||||
full_snapshot_name,
|
||||
module.os.path.normpath(snapshot_mount_path),
|
||||
).once()
|
||||
source_directories = ['/mnt/dataset']
|
||||
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
hook_config={},
|
||||
config={'source_directories': '/mnt/dataset', 'zfs': {}},
|
||||
log_prefix='test',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=source_directories,
|
||||
dry_run=False,
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
assert source_directories == [snapshot_mount_path]
|
||||
|
||||
|
||||
def test_dump_data_sources_uses_custom_commands():
|
||||
flexmock(module).should_receive('get_datasets_to_backup').and_return(
|
||||
(('dataset', '/mnt/dataset'),)
|
||||
)
|
||||
flexmock(module.os).should_receive('getpid').and_return(1234)
|
||||
full_snapshot_name = 'dataset@borgmatic-1234'
|
||||
flexmock(module).should_receive('snapshot_dataset').with_args(
|
||||
'/usr/local/bin/zfs',
|
||||
full_snapshot_name,
|
||||
).once()
|
||||
snapshot_mount_path = '/run/borgmatic/zfs_snapshots/./mnt/dataset'
|
||||
flexmock(module).should_receive('mount_snapshot').with_args(
|
||||
'/usr/local/bin/mount',
|
||||
full_snapshot_name,
|
||||
module.os.path.normpath(snapshot_mount_path),
|
||||
).once()
|
||||
source_directories = ['/mnt/dataset']
|
||||
hook_config = {
|
||||
'zfs_command': '/usr/local/bin/zfs',
|
||||
'mount_command': '/usr/local/bin/mount',
|
||||
}
|
||||
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
hook_config=hook_config,
|
||||
config={
|
||||
'source_directories': source_directories,
|
||||
'zfs': hook_config,
|
||||
},
|
||||
log_prefix='test',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=source_directories,
|
||||
dry_run=False,
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
assert source_directories == [snapshot_mount_path]
|
||||
|
||||
|
||||
def test_dump_data_sources_with_dry_run_skips_commands_and_does_not_touch_source_directories():
|
||||
flexmock(module).should_receive('get_datasets_to_backup').and_return(
|
||||
(('dataset', '/mnt/dataset'),)
|
||||
)
|
||||
flexmock(module.os).should_receive('getpid').and_return(1234)
|
||||
flexmock(module).should_receive('snapshot_dataset').never()
|
||||
flexmock(module).should_receive('mount_snapshot').never()
|
||||
source_directories = ['/mnt/dataset']
|
||||
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
hook_config={},
|
||||
config={'source_directories': '/mnt/dataset', 'zfs': {}},
|
||||
log_prefix='test',
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
source_directories=source_directories,
|
||||
dry_run=True,
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
assert source_directories == ['/mnt/dataset']
|
||||
|
||||
|
||||
def test_get_all_snapshots_parses_list_output():
|
||||
flexmock(borgmatic.execute).should_receive('execute_command_and_capture_output').and_return(
|
||||
'dataset1@borgmatic-1234\ndataset2@borgmatic-4567',
|
||||
)
|
||||
|
||||
assert module.get_all_snapshots('zfs') == ('dataset1@borgmatic-1234', 'dataset2@borgmatic-4567')
|
||||
|
||||
|
||||
def test_remove_data_source_dumps_unmounts_and_destroys_snapshots():
|
||||
flexmock(module).should_receive('get_all_datasets').and_return((('dataset', '/mnt/dataset'),))
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'replace_temporary_subdirectory_with_glob'
|
||||
).and_return('/run/borgmatic')
|
||||
flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
|
||||
flexmock(module.os.path).should_receive('isdir').and_return(True)
|
||||
flexmock(module.shutil).should_receive('rmtree')
|
||||
flexmock(module).should_receive('unmount_snapshot').with_args(
|
||||
'umount', '/run/borgmatic/zfs_snapshots/mnt/dataset'
|
||||
).once()
|
||||
flexmock(module).should_receive('get_all_snapshots').and_return(
|
||||
('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid')
|
||||
)
|
||||
flexmock(module).should_receive('destroy_snapshot').with_args(
|
||||
'zfs', 'dataset@borgmatic-1234'
|
||||
).once()
|
||||
|
||||
module.remove_data_source_dumps(
|
||||
hook_config={},
|
||||
config={'source_directories': '/mnt/dataset', 'zfs': {}},
|
||||
log_prefix='test',
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_remove_data_source_dumps_use_custom_commands():
|
||||
flexmock(module).should_receive('get_all_datasets').and_return((('dataset', '/mnt/dataset'),))
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'replace_temporary_subdirectory_with_glob'
|
||||
).and_return('/run/borgmatic')
|
||||
flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
|
||||
flexmock(module.os.path).should_receive('isdir').and_return(True)
|
||||
flexmock(module.shutil).should_receive('rmtree')
|
||||
flexmock(module).should_receive('unmount_snapshot').with_args(
|
||||
'/usr/local/bin/umount', '/run/borgmatic/zfs_snapshots/mnt/dataset'
|
||||
).once()
|
||||
flexmock(module).should_receive('get_all_snapshots').and_return(
|
||||
('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid')
|
||||
)
|
||||
flexmock(module).should_receive('destroy_snapshot').with_args(
|
||||
'/usr/local/bin/zfs', 'dataset@borgmatic-1234'
|
||||
).once()
|
||||
hook_config = {'zfs_command': '/usr/local/bin/zfs', 'umount_command': '/usr/local/bin/umount'}
|
||||
|
||||
module.remove_data_source_dumps(
|
||||
hook_config=hook_config,
|
||||
config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
|
||||
log_prefix='test',
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_remove_data_source_dumps_bails_for_missing_zfs_command():
|
||||
flexmock(module).should_receive('get_all_datasets').and_raise(FileNotFoundError)
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'replace_temporary_subdirectory_with_glob'
|
||||
).never()
|
||||
hook_config = {'zfs_command': 'wtf'}
|
||||
|
||||
module.remove_data_source_dumps(
|
||||
hook_config=hook_config,
|
||||
config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
|
||||
log_prefix='test',
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_remove_data_source_dumps_bails_for_zfs_command_error():
|
||||
flexmock(module).should_receive('get_all_datasets').and_raise(
|
||||
module.subprocess.CalledProcessError(1, 'wtf')
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'replace_temporary_subdirectory_with_glob'
|
||||
).never()
|
||||
hook_config = {'zfs_command': 'wtf'}
|
||||
|
||||
module.remove_data_source_dumps(
|
||||
hook_config=hook_config,
|
||||
config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
|
||||
log_prefix='test',
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_remove_data_source_dumps_bails_for_missing_umount_command():
|
||||
flexmock(module).should_receive('get_all_datasets').and_return((('dataset', '/mnt/dataset'),))
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'replace_temporary_subdirectory_with_glob'
|
||||
).and_return('/run/borgmatic')
|
||||
flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
|
||||
flexmock(module.os.path).should_receive('isdir').and_return(True)
|
||||
flexmock(module.shutil).should_receive('rmtree')
|
||||
flexmock(module).should_receive('unmount_snapshot').with_args(
|
||||
'/usr/local/bin/umount', '/run/borgmatic/zfs_snapshots/mnt/dataset'
|
||||
).and_raise(FileNotFoundError)
|
||||
flexmock(module).should_receive('get_all_snapshots').never()
|
||||
flexmock(module).should_receive('destroy_snapshot').never()
|
||||
hook_config = {'zfs_command': '/usr/local/bin/zfs', 'umount_command': '/usr/local/bin/umount'}
|
||||
|
||||
module.remove_data_source_dumps(
|
||||
hook_config=hook_config,
|
||||
config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
|
||||
log_prefix='test',
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_remove_data_source_dumps_bails_for_umount_command_error():
|
||||
flexmock(module).should_receive('get_all_datasets').and_return((('dataset', '/mnt/dataset'),))
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'replace_temporary_subdirectory_with_glob'
|
||||
).and_return('/run/borgmatic')
|
||||
flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
|
||||
flexmock(module.os.path).should_receive('isdir').and_return(True)
|
||||
flexmock(module.shutil).should_receive('rmtree')
|
||||
flexmock(module).should_receive('unmount_snapshot').with_args(
|
||||
'/usr/local/bin/umount', '/run/borgmatic/zfs_snapshots/mnt/dataset'
|
||||
).and_raise(module.subprocess.CalledProcessError(1, 'wtf'))
|
||||
flexmock(module).should_receive('get_all_snapshots').never()
|
||||
flexmock(module).should_receive('destroy_snapshot').never()
|
||||
hook_config = {'zfs_command': '/usr/local/bin/zfs', 'umount_command': '/usr/local/bin/umount'}
|
||||
|
||||
module.remove_data_source_dumps(
|
||||
hook_config=hook_config,
|
||||
config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
|
||||
log_prefix='test',
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_remove_data_source_dumps_skips_unmount_snapshot_directories_that_are_not_actually_directories():
|
||||
flexmock(module).should_receive('get_all_datasets').and_return((('dataset', '/mnt/dataset'),))
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'replace_temporary_subdirectory_with_glob'
|
||||
).and_return('/run/borgmatic')
|
||||
flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
|
||||
flexmock(module.os.path).should_receive('isdir').and_return(False)
|
||||
flexmock(module.shutil).should_receive('rmtree').never()
|
||||
flexmock(module).should_receive('unmount_snapshot').never()
|
||||
flexmock(module).should_receive('get_all_snapshots').and_return(
|
||||
('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid')
|
||||
)
|
||||
flexmock(module).should_receive('destroy_snapshot').with_args(
|
||||
'zfs', 'dataset@borgmatic-1234'
|
||||
).once()
|
||||
|
||||
module.remove_data_source_dumps(
|
||||
hook_config={},
|
||||
config={'source_directories': '/mnt/dataset', 'zfs': {}},
|
||||
log_prefix='test',
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_that_are_not_actually_directories():
|
||||
flexmock(module).should_receive('get_all_datasets').and_return((('dataset', '/mnt/dataset'),))
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'replace_temporary_subdirectory_with_glob'
|
||||
).and_return('/run/borgmatic')
|
||||
flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
|
||||
flexmock(module.os.path).should_receive('isdir').with_args(
|
||||
'/run/borgmatic/zfs_snapshots'
|
||||
).and_return(True)
|
||||
flexmock(module.os.path).should_receive('isdir').with_args(
|
||||
'/run/borgmatic/zfs_snapshots/mnt/dataset'
|
||||
).and_return(False)
|
||||
flexmock(module.shutil).should_receive('rmtree')
|
||||
flexmock(module).should_receive('unmount_snapshot').never()
|
||||
flexmock(module).should_receive('get_all_snapshots').and_return(
|
||||
('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid')
|
||||
)
|
||||
flexmock(module).should_receive('destroy_snapshot').with_args(
|
||||
'zfs', 'dataset@borgmatic-1234'
|
||||
).once()
|
||||
|
||||
module.remove_data_source_dumps(
|
||||
hook_config={},
|
||||
config={'source_directories': '/mnt/dataset', 'zfs': {}},
|
||||
log_prefix='test',
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_remove_data_source_dumps_with_dry_run_skips_unmount_and_destroy():
|
||||
flexmock(module).should_receive('get_all_datasets').and_return((('dataset', '/mnt/dataset'),))
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'replace_temporary_subdirectory_with_glob'
|
||||
).and_return('/run/borgmatic')
|
||||
flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
|
||||
flexmock(module.os.path).should_receive('isdir').and_return(True)
|
||||
flexmock(module.shutil).should_receive('rmtree').never()
|
||||
flexmock(module).should_receive('unmount_snapshot').never()
|
||||
flexmock(module).should_receive('get_all_snapshots').and_return(
|
||||
('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid')
|
||||
)
|
||||
flexmock(module).should_receive('destroy_snapshot').never()
|
||||
|
||||
module.remove_data_source_dumps(
|
||||
hook_config={},
|
||||
config={'source_directories': '/mnt/dataset', 'zfs': {}},
|
||||
log_prefix='test',
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
dry_run=True,
|
||||
)
|
||||
@@ -45,11 +45,13 @@ def test_interactive_console_true_when_isatty_and_TERM_is_not_dumb(capsys):
|
||||
|
||||
|
||||
def test_should_do_markup_respects_no_color_value():
|
||||
flexmock(module.os.environ).should_receive('get').and_return(None)
|
||||
flexmock(module).should_receive('interactive_console').never()
|
||||
assert module.should_do_markup(no_color=True, configs={}) is False
|
||||
|
||||
|
||||
def test_should_do_markup_respects_config_value():
|
||||
flexmock(module.os.environ).should_receive('get').and_return(None)
|
||||
flexmock(module).should_receive('interactive_console').never()
|
||||
assert module.should_do_markup(no_color=False, configs={'foo.yaml': {'color': False}}) is False
|
||||
|
||||
@@ -58,6 +60,7 @@ def test_should_do_markup_respects_config_value():
|
||||
|
||||
|
||||
def test_should_do_markup_prefers_any_false_config_value():
|
||||
flexmock(module.os.environ).should_receive('get').and_return(None)
|
||||
flexmock(module).should_receive('interactive_console').never()
|
||||
|
||||
assert (
|
||||
@@ -84,6 +87,7 @@ def test_should_do_markup_respects_PY_COLORS_environment_variable():
|
||||
|
||||
|
||||
def test_should_do_markup_prefers_no_color_value_to_config_value():
|
||||
flexmock(module.os.environ).should_receive('get').and_return(None)
|
||||
flexmock(module).should_receive('interactive_console').never()
|
||||
|
||||
assert module.should_do_markup(no_color=True, configs={'foo.yaml': {'color': True}}) is False
|
||||
|
||||
Reference in New Issue
Block a user