Compare commits

...
52 Commits
Author SHA1 Message Date
Dan Helfman 81cd03cbbf Bump version for release. 2024-12-06 20:29:16 -08:00
Dan Helfman f2455527fc Fix spelling in comment (#80). 2024-12-06 20:22:45 -08:00
Dan Helfman 62d67cde0a LVM snapshots + ZFS and Btrfs improvements (#80).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/949
2024-12-07 04:21:22 +00:00
Dan Helfman ae8a9db27d Fix flake issues (#80). 2024-12-06 16:12:01 -08:00
Dan Helfman 8979f8918d Organize imports (#80). 2024-12-06 16:05:46 -08:00
Dan Helfman eb97708092 Completed tests for LVM (#80). 2024-12-06 16:02:33 -08:00
Dan Helfman f2d93b85b4 Lots of LVM unit tests + code formatting (#80). 2024-12-06 13:59:38 -08:00
Dan Helfman b999d2dc4d Add some missing test coverage (#80). 2024-12-06 10:27:47 -08:00
Dan Helfman 7f2e38d061 Fix file permissions (#80). 2024-12-06 09:40:32 -08:00
Dan Helfman 140fc248b6 Fix LVM end-to-end tests (#80). 2024-12-06 09:39:24 -08:00
Dan Helfman ec9e1a8223 LVM hook end-to-end tests, not quite working yet (#80). 2024-12-05 22:46:50 -08:00
Dan Helfman 03bbe77dd9 Add an end-to-end test for the Btrfs hook using a fake Btrfs binary (#80). 2024-12-05 17:35:44 -08:00
Dan Helfman f1c5f11422 Add an end-to-end test for the ZFS hook using a fake ZFS binary (#80). 2024-12-05 11:18:53 -08:00
Dan Helfman f8df06fb92 Remove divison by zero (#80). 2024-12-04 20:33:59 -08:00
Dan Helfman d95707ff9b Get existing tests passing (#80). 2024-12-04 20:22:59 -08:00
Dan Helfman 51a7f50e3a Add ZFS snapshot unmount error fix to NEWS (#950). 2024-12-04 15:43:05 -08:00
Dan Helfman 49b8b693af Don't try to unmount a ZFS snapshot if it's already deleted (#80). 2024-12-04 15:39:04 -08:00
Dan Helfman d0e92493f6 Fix broken ZFS tests (#80). 2024-12-04 14:48:13 -08:00
Dan Helfman 9afdaca985 Before unmounting, remove the snapshot mount path instead of the parent snapshot directory (#80). 2024-12-03 19:19:22 -08:00
Dan Helfman cc11ed78e0 Put LVM snapshots into a data structure for convenience (#80). 2024-12-03 19:12:41 -08:00
Dan Helfman 87f3746881 Fix a ZFS edge case in which the hook tries to unmounted a non-mounted directory (#80). 2024-12-03 15:56:03 -08:00
Dan Helfman 347a4c3dd5 Fix breakage of ZFS user property auto-backup (#80). 2024-12-03 15:43:50 -08:00
Dan Helfman 399bb6ef68 Add recent LVM and ZFS work to NEWS (#80). 2024-12-03 12:22:43 -08:00
Dan Helfman 9b9ecad299 Port the parent directory discovery logic from LVM to Btrfs (#80). 2024-12-03 12:15:34 -08:00
Dan Helfman 8c4b899a13 Use a namedtuple for logical volume metadata (#80). 2024-12-03 11:12:27 -08:00
Dan Helfman 9b77de3d66 Port the parent directory discovery logic from LVM to ZFS (#80). 2024-12-03 11:05:45 -08:00
Dan Helfman bfeea5d394 Code formatting (#80). 2024-12-03 08:52:05 -08:00
Dan Helfman 8a6225b7c2 Factor out logic for finding contained source directories in a parent directory (#80). 2024-12-03 08:51:10 -08:00
Dan Helfman 9aaa3c925f Code formatting (#80). 2024-12-02 21:01:34 -08:00
Dan Helfman 88fd1ae454 Discover parent/grandparent/etc. logical volumes of source directories (#80). 2024-12-02 20:58:50 -08:00
Dan Helfman 27305ec2bf Clarify the path rewriting for LVM (but also ZFS + Btrfs) (#80). 2024-12-02 12:01:04 -08:00
Dan Helfman 4453c2d49c Add LVM logo to integrations docs. 2024-12-02 11:28:57 -08:00
Dan Helfman 6367a00013 Add snapshot_size option (#80). 2024-12-02 11:09:07 -08:00
Dan Helfman cd654cbb57 Fix a few docstring typos (#80). 2024-12-01 21:00:11 -08:00
Dan Helfman 1e8f73779f Fix typo in schema comment (#80). 2024-12-01 20:25:16 -08:00
Dan Helfman 27d167b071 LVM snapshots WIP (#80). 2024-12-01 20:13:02 -08:00
Dan Helfman cfff6c6855 Btrfs snapshotting (#251).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/946
2024-11-30 19:19:09 +00:00
Dan Helfman 37efaeae88 Warn if Btrfs is configured but there are no Btrfs subvolumes detected (#251). 2024-11-30 10:55:30 -08:00
Dan Helfman 0978c669ad A little more Btrfs error handling (#251). 2024-11-30 10:25:01 -08:00
Dan Helfman 1366269586 Add a couple of missing tests (#251). 2024-11-30 09:44:55 -08:00
Dan Helfman a9a0910817 Add Btrfs logo to integrations docs (#251). 2024-11-30 09:36:52 -08:00
Dan Helfman 5bcc7b60c8 Tests for Btrfs (#251). 2024-11-30 09:32:50 -08:00
Dan Helfman 84a0552277 Improve Btrfs hook factoring/organization (#251). 2024-11-29 09:36:46 -08:00
Dan Helfman d4a02f73b5 Create Btrfs snapshots as read-only (#251). 2024-11-28 22:18:44 -08:00
Dan Helfman 3f901c0a52 Btrfs hook documentation (#251). 2024-11-28 20:32:12 -08:00
Dan Helfman b5b5c1fafa Initial work on a Btrfs hook (#251). 2024-11-28 18:47:15 -08:00
Dan Helfman 86e5085acc Fix incorrect documentation links to source. 2024-11-27 08:54:19 -08:00
Dan Helfman 08a5e8717b Merge branch 'main' of ssh://projects.torsion.org:3022/borgmatic-collective/borgmatic 2024-11-27 08:51:00 -08:00
Dan Helfman 6b2f2b2ac4 Reorganize data source and monitoring hooks to make developing new hooks easier. 2024-11-27 08:50:34 -08:00
Dan Helfman a07cf9e699 Revert temporary reversion of 1.9.4.dev0.
revert Temporary revert of 1.9.4.dev0 changeset so we can re-build 1.9.3 (which never actually got built).

revert Fix library error when running within a PyInstaller bundle (#926).
2024-11-26 16:20:06 +00:00
Dan Helfman bf40b01077 Temporary revert of 1.9.4.dev0 changeset so we can re-build 1.9.3 (which never actually got built).
revert Fix library error when running within a PyInstaller bundle (#926).
2024-11-26 16:13:39 +00:00
Dan Helfman a5c6a2fe1c Fix library error when running within a PyInstaller bundle (#926). 2024-11-25 20:14:18 -08:00
87 changed files with 4443 additions and 482 deletions
+13
View File
@@ -1,3 +1,16 @@
1.9.4
* #80 (beta): Add an LVM hook for snapshotting and backing up LVM logical volumes. See the
documentation for more information:
https://torsion.org/borgmatic/docs/how-to/snapshot-your-filesystems/
* #251 (beta): Add a Btrfs hook for snapshotting and backing up Btrfs subvolumes. See the
documentation for more information:
https://torsion.org/borgmatic/docs/how-to/snapshot-your-filesystems/
* #926: Fix a library error when running within a PyInstaller bundle.
* #950: Fix a snapshot unmount error in the ZFS hook when using nested datasets.
* Update the ZFS hook to discover and snapshot ZFS datasets even if they are parent/grandparent
directories of your source directories.
* Reorganize data source and monitoring hooks to make developing new hooks easier.
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/
+2
View File
@@ -62,6 +62,8 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
<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://btrfs.readthedocs.io/"><img src="docs/static/btrfs.png" alt="Btrfs" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://sourceware.org/lvm2/"><img src="docs/static/lvm.png" alt="LVM" 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>
+1 -1
View File
@@ -364,7 +364,7 @@ def collect_spot_check_source_paths(
'use_streaming',
config,
repository['path'],
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
).values()
)
+3 -4
View File
@@ -10,7 +10,6 @@ import borgmatic.config.paths
import borgmatic.config.validate
import borgmatic.hooks.command
import borgmatic.hooks.dispatch
import borgmatic.hooks.dump
logger = logging.getLogger(__name__)
@@ -186,7 +185,7 @@ def run_create(
'remove_data_source_dumps',
config,
repository['path'],
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
borgmatic_runtime_directory,
global_arguments.dry_run,
)
@@ -195,7 +194,7 @@ def run_create(
'dump_data_sources',
config,
repository['path'],
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
config_paths,
borgmatic_runtime_directory,
source_directories,
@@ -232,7 +231,7 @@ def run_create(
'remove_data_source_dumps',
config,
config_filename,
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
borgmatic_runtime_directory,
global_arguments.dry_run,
)
+17 -10
View File
@@ -11,8 +11,8 @@ import borgmatic.borg.mount
import borgmatic.borg.repo_list
import borgmatic.config.paths
import borgmatic.config.validate
import borgmatic.hooks.data_source.dump
import borgmatic.hooks.dispatch
import borgmatic.hooks.dump
logger = logging.getLogger(__name__)
@@ -44,7 +44,8 @@ def get_configured_data_source(
hooks_to_search = {
hook_name: value
for (hook_name, value) in config.items()
if hook_name in borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES
if hook_name.split('_databases')[0]
in borgmatic.hooks.dispatch.get_submodule_names(borgmatic.hooks.data_source)
}
else:
try:
@@ -123,10 +124,10 @@ def restore_single_data_source(
'make_data_source_dump_patterns',
config,
repository['path'],
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
borgmatic_runtime_directory,
data_source['name'],
)[hook_name]
)[hook_name.split('_databases')[0]]
destination_path = (
tempfile.mkdtemp(dir=borgmatic_runtime_directory)
@@ -141,7 +142,11 @@ def restore_single_data_source(
dry_run=global_arguments.dry_run,
repository=repository['path'],
archive=archive_name,
paths=[borgmatic.hooks.dump.convert_glob_patterns_to_borg_pattern(dump_patterns)],
paths=[
borgmatic.hooks.data_source.dump.convert_glob_patterns_to_borg_pattern(
dump_patterns
)
],
config=config,
local_borg_version=local_borg_version,
global_arguments=global_arguments,
@@ -162,11 +167,11 @@ def restore_single_data_source(
shutil.rmtree(destination_path, ignore_errors=True)
# Run a single data source restore, consuming the extract stdout (if any).
borgmatic.hooks.dispatch.call_hooks(
borgmatic.hooks.dispatch.call_hook(
function_name='restore_data_source_dump',
config=config,
log_prefix=repository['path'],
hook_names=[hook_name],
hook_name=hook_name,
data_source=data_source,
dry_run=global_arguments.dry_run,
extract_process=extract_process,
@@ -206,7 +211,9 @@ def collect_archive_data_source_names(
global_arguments,
list_paths=[
'sh:'
+ borgmatic.hooks.dump.make_data_source_dump_path(base_directory, '*_databases/*/*')
+ borgmatic.hooks.data_source.dump.make_data_source_dump_path(
base_directory, '*_databases/*/*'
)
for base_directory in (
'borgmatic',
borgmatic.config.paths.make_runtime_directory_glob(borgmatic_runtime_directory),
@@ -354,7 +361,7 @@ def run_restore(
'remove_data_source_dumps',
config,
repository['path'],
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
borgmatic_runtime_directory,
global_arguments.dry_run,
)
@@ -451,7 +458,7 @@ def run_restore(
'remove_data_source_dumps',
config,
repository['path'],
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
borgmatic_runtime_directory,
global_arguments.dry_run,
)
+9 -8
View File
@@ -39,7 +39,8 @@ from borgmatic.borg import umount as borg_umount
from borgmatic.borg import version as borg_version
from borgmatic.commands.arguments import parse_arguments
from borgmatic.config import checks, collect, validate
from borgmatic.hooks import command, dispatch, monitor
from borgmatic.hooks import command, dispatch
from borgmatic.hooks.monitoring import monitor
from borgmatic.logger import DISABLED, add_custom_log_levels, configure_logging, should_do_markup
from borgmatic.signals import configure_signals
from borgmatic.verbosity import verbosity_to_log_level
@@ -103,7 +104,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
'initialize_monitor',
config,
config_filename,
monitor.MONITOR_HOOK_NAMES,
dispatch.Hook_type.MONITORING,
monitoring_log_level,
global_arguments.dry_run,
)
@@ -112,7 +113,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
'ping_monitor',
config,
config_filename,
monitor.MONITOR_HOOK_NAMES,
dispatch.Hook_type.MONITORING,
monitor.State.START,
monitoring_log_level,
global_arguments.dry_run,
@@ -188,7 +189,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
'ping_monitor',
config,
config_filename,
monitor.MONITOR_HOOK_NAMES,
dispatch.Hook_type.MONITORING,
monitor.State.LOG,
monitoring_log_level,
global_arguments.dry_run,
@@ -205,7 +206,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
'ping_monitor',
config,
config_filename,
monitor.MONITOR_HOOK_NAMES,
dispatch.Hook_type.MONITORING,
monitor.State.FINISH,
monitoring_log_level,
global_arguments.dry_run,
@@ -214,7 +215,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
'destroy_monitor',
config,
config_filename,
monitor.MONITOR_HOOK_NAMES,
dispatch.Hook_type.MONITORING,
monitoring_log_level,
global_arguments.dry_run,
)
@@ -241,7 +242,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
'ping_monitor',
config,
config_filename,
monitor.MONITOR_HOOK_NAMES,
dispatch.Hook_type.MONITORING,
monitor.State.FAIL,
monitoring_log_level,
global_arguments.dry_run,
@@ -250,7 +251,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
'destroy_monitor',
config,
config_filename,
monitor.MONITOR_HOOK_NAMES,
dispatch.Hook_type.MONITORING,
monitoring_log_level,
global_arguments.dry_run,
)
+8 -5
View File
@@ -33,10 +33,13 @@ def get_borgmatic_source_directory(config):
TEMPORARY_DIRECTORY_PREFIX = 'borgmatic-'
def replace_temporary_subdirectory_with_glob(path):
def replace_temporary_subdirectory_with_glob(
path, temporary_directory_prefix=TEMPORARY_DIRECTORY_PREFIX
):
'''
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:
Given an absolute temporary directory path and an optional temporary directory prefix, look for
a subdirectory within it starting with the temporary directory prefix (or a default) and replace
it with an appropriate glob. For instance, given:
/tmp/borgmatic-aet8kn93/borgmatic
@@ -50,8 +53,8 @@ def replace_temporary_subdirectory_with_glob(path):
'/',
*(
(
f'{TEMPORARY_DIRECTORY_PREFIX}*'
if subdirectory.startswith(TEMPORARY_DIRECTORY_PREFIX)
f'{temporary_directory_prefix}*'
if subdirectory.startswith(temporary_directory_prefix)
else subdirectory
)
for subdirectory in path.split(os.path.sep)
+63
View File
@@ -2288,3 +2288,66 @@ properties:
example: /usr/local/bin/umount
description: |
Configuration for integration with the ZFS filesystem.
btrfs:
type: ["object", "null"]
additionalProperties: false
properties:
btrfs_command:
type: string
description: |
Command to use instead of "btrfs".
example: /usr/local/bin/btrfs
findmnt_command:
type: string
description: |
Command to use instead of "findmnt".
example: /usr/local/bin/findmnt
description: |
Configuration for integration with the Btrfs filesystem.
lvm:
type: ["object", "null"]
additionalProperties: false
properties:
snapshot_size:
type: string
description: |
Size to allocate for each snapshot taken, including the
units to use for that size. Defaults to "10%ORIGIN" (10%
of the size of logical volume being snapshotted). See the
lvcreate "--size" and "--extents" documentation for more
information:
https://www.man7.org/linux/man-pages/man8/lvcreate.8.html
example: 5GB
lvcreate_command:
type: string
description: |
Command to use instead of "lvcreate".
example: /usr/local/bin/lvcreate
lvremove_command:
type: string
description: |
Command to use instead of "lvremove".
example: /usr/local/bin/lvremove
lvs_command:
type: string
description: |
Command to use instead of "lvs".
example: /usr/local/bin/lvs
lsblk_command:
type: string
description: |
Command to use instead of "lsblk".
example: /usr/local/bin/lsblk
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 Linux LVM (Logical Volume
Manager).
+25 -9
View File
@@ -2,8 +2,9 @@ import logging
import os
import re
import shlex
import sys
from borgmatic import execute
import borgmatic.execute
logger = logging.getLogger(__name__)
@@ -27,6 +28,20 @@ def interpolate_context(config_filename, hook_description, command, context):
return command
def make_environment(current_environment, sys_module=sys):
'''
Given the existing system environment as a map from environment variable name to value, return
(in the same form) any extra environment variables that should be used when running command
hooks.
'''
# Detect whether we're running within a PyInstaller bundle. If so, set or clear LD_LIBRARY_PATH
# based on the value of LD_LIBRARY_PATH_ORIG. This prevents library version information errors.
if getattr(sys_module, 'frozen', False) and hasattr(sys_module, '_MEIPASS'):
return {'LD_LIBRARY_PATH': current_environment.get('LD_LIBRARY_PATH_ORIG', '')}
return {}
def execute_hook(commands, umask, config_filename, description, dry_run, **context):
'''
Given a list of hook commands to execute, a umask to execute with (or None), a config filename,
@@ -65,14 +80,15 @@ def execute_hook(commands, umask, config_filename, description, dry_run, **conte
try:
for command in commands:
if not dry_run:
execute.execute_command(
[command],
output_log_level=(
logging.ERROR if description == 'on-error' else logging.WARNING
),
shell=True,
)
if dry_run:
continue
borgmatic.execute.execute_command(
[command],
output_log_level=(logging.ERROR if description == 'on-error' else logging.WARNING),
shell=True,
extra_environment=make_environment(os.environ),
)
finally:
if original_umask:
os.umask(original_umask)
+333
View File
@@ -0,0 +1,333 @@
import collections
import glob
import logging
import os
import shutil
import subprocess
import borgmatic.config.paths
import borgmatic.execute
import borgmatic.hooks.data_source.snapshot
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 get_filesystem_mount_points(findmnt_command):
'''
Given a findmnt command to run, get all top-level Btrfs filesystem mount points.
'''
findmnt_output = borgmatic.execute.execute_command_and_capture_output(
tuple(findmnt_command.split(' '))
+ (
'-n', # No headings.
'-t', # Filesystem type.
'btrfs',
)
)
return tuple(line.rstrip().split(' ')[0] for line in findmnt_output.splitlines())
def get_subvolumes_for_filesystem(btrfs_command, filesystem_mount_point):
'''
Given a Btrfs command to run and a Btrfs filesystem mount point, get the sorted subvolumes for
that filesystem. Include the filesystem itself.
'''
btrfs_output = borgmatic.execute.execute_command_and_capture_output(
tuple(btrfs_command.split(' '))
+ (
'subvolume',
'list',
filesystem_mount_point,
)
)
if not filesystem_mount_point.strip():
return ()
return (filesystem_mount_point,) + tuple(
sorted(
subvolume_path
for line in btrfs_output.splitlines()
for subvolume_subpath in (line.rstrip().split(' ')[-1],)
for subvolume_path in (os.path.join(filesystem_mount_point, subvolume_subpath),)
if subvolume_subpath.strip()
)
)
Subvolume = collections.namedtuple(
'Subvolume', ('path', 'contained_source_directories'), defaults=((),)
)
def get_subvolumes(btrfs_command, findmnt_command, source_directories=None):
'''
Given a Btrfs command to run and a sequence of configured source directories, find the
intersection between the current Btrfs filesystem and subvolume mount points and the configured
borgmatic source directories. The idea is that these are the requested subvolumes to snapshot.
If the source directories is None, then return all subvolumes, sorted by path.
Return the result as a sequence of matching subvolume mount points.
'''
candidate_source_directories = set(source_directories or ())
subvolumes = []
# For each filesystem mount point, find its subvolumes and match them again the given source
# directories to find the subvolumes to backup. And within this loop, sort the subvolumes from
# longest to shortest mount points, so longer mount points get a whack at the candidate source
# directory piñata before their parents do. (Source directories are consumed during this
# process, so no two datasets get the same contained source directories.)
for mount_point in get_filesystem_mount_points(findmnt_command):
subvolumes.extend(
Subvolume(subvolume_path, contained_source_directories)
for subvolume_path in reversed(
get_subvolumes_for_filesystem(btrfs_command, mount_point)
)
for contained_source_directories in (
borgmatic.hooks.data_source.snapshot.get_contained_directories(
subvolume_path, candidate_source_directories
),
)
if source_directories is None or contained_source_directories
)
return tuple(sorted(subvolumes, key=lambda subvolume: subvolume.path))
BORGMATIC_SNAPSHOT_PREFIX = '.borgmatic-snapshot-'
def make_snapshot_path(subvolume_path): # pragma: no cover
'''
Given the path to a subvolume, make a corresponding snapshot path for it.
'''
return os.path.join(
subvolume_path,
f'{BORGMATIC_SNAPSHOT_PREFIX}{os.getpid()}',
# Included so that the snapshot ends up in the Borg archive at the "original" subvolume
# path.
subvolume_path.lstrip(os.path.sep),
)
def make_snapshot_exclude_path(subvolume_path): # pragma: no cover
'''
Given the path to a subvolume, make a corresponding exclude path for its embedded snapshot path.
This is to work around a quirk of Btrfs: If you make a snapshot path as a child directory of a
subvolume, then the snapshot's own initial directory component shows up as an empty directory
within the snapshot itself. For instance, if you have a Btrfs subvolume at /mnt and make a
snapshot of it at:
/mnt/.borgmatic-snapshot-1234/mnt
... then the snapshot itself will have an empty directory at:
/mnt/.borgmatic-snapshot-1234/mnt/.borgmatic-snapshot-1234
So to prevent that from ending up in the Borg archive, this function produces its path for
exclusion.
'''
snapshot_directory = f'{BORGMATIC_SNAPSHOT_PREFIX}{os.getpid()}'
return os.path.join(
subvolume_path,
snapshot_directory,
subvolume_path.lstrip(os.path.sep),
snapshot_directory,
)
def make_borg_source_directory_path(subvolume_path, source_directory): # pragma: no cover
'''
Given the path to a subvolume and a source directory inside it, make a corresponding path for
the source directory within a snapshot path intended for giving to Borg.
'''
return os.path.join(
subvolume_path,
f'{BORGMATIC_SNAPSHOT_PREFIX}{os.getpid()}',
'.', # Borg 1.4+ "slashdot" hack.
# Included so that the source directory ends up in the Borg archive at its "original" path.
source_directory.lstrip(os.path.sep),
)
def snapshot_subvolume(btrfs_command, subvolume_path, snapshot_path): # pragma: no cover
'''
Given a Btrfs command to run, the path to a subvolume, and the path for a snapshot, create a new
Btrfs snapshot of the subvolume.
'''
os.makedirs(os.path.dirname(snapshot_path), mode=0o700, exist_ok=True)
borgmatic.execute.execute_command(
tuple(btrfs_command.split(' '))
+ (
'subvolume',
'snapshot',
'-r', # Read-only,
subvolume_path,
snapshot_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 Btrfs 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 Btrfs subvolume mount points listed
in the given source directories. Also update those source directories, replacing subvolume mount
points with corresponding snapshot directories so they get stored in the Borg archive instead.
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 Btrfs subvolumes{dry_run_label}')
# Based on the configured source directories, determine Btrfs subvolumes to backup.
btrfs_command = hook_config.get('btrfs_command', 'btrfs')
findmnt_command = hook_config.get('findmnt_command', 'findmnt')
subvolumes = get_subvolumes(btrfs_command, findmnt_command, source_directories)
if not subvolumes:
logger.warning(f'{log_prefix}: No Btrfs subvolumes found to snapshot{dry_run_label}')
# Snapshot each subvolume, rewriting source directories to use their snapshot paths.
for subvolume in subvolumes:
logger.debug(f'{log_prefix}: Creating Btrfs snapshot for {subvolume.path} subvolume')
snapshot_path = make_snapshot_path(subvolume.path)
if dry_run:
continue
snapshot_subvolume(btrfs_command, subvolume.path, snapshot_path)
for source_directory in subvolume.contained_source_directories:
try:
source_directories.remove(source_directory)
except ValueError:
pass
source_directories.append(
make_borg_source_directory_path(subvolume.path, source_directory)
)
config.setdefault('exclude_patterns', []).append(make_snapshot_exclude_path(subvolume.path))
return []
def delete_snapshot(btrfs_command, snapshot_path): # pragma: no cover
'''
Given a Btrfs command to run and the name of a snapshot path, delete it.
'''
borgmatic.execute.execute_command(
tuple(btrfs_command.split(' '))
+ (
'subvolume',
'delete',
snapshot_path,
),
output_log_level=logging.DEBUG,
)
def remove_data_source_dumps(hook_config, config, log_prefix, borgmatic_runtime_directory, dry_run):
'''
Given a Btrfs configuration dict, a configuration dict, a log prefix, the borgmatic runtime
directory, and whether this is a dry run, delete any Btrfs 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 ''
btrfs_command = hook_config.get('btrfs_command', 'btrfs')
findmnt_command = hook_config.get('findmnt_command', 'findmnt')
try:
all_subvolumes = get_subvolumes(btrfs_command, findmnt_command)
except FileNotFoundError as error:
logger.debug(f'{log_prefix}: Could not find "{error.filename}" command')
return
except subprocess.CalledProcessError as error:
logger.debug(f'{log_prefix}: {error}')
return
# Reversing the sorted subvolumes ensures that we remove longer mount point paths of child
# subvolumes before the shorter mount point paths of parent subvolumes.
for subvolume in reversed(all_subvolumes):
subvolume_snapshots_glob = borgmatic.config.paths.replace_temporary_subdirectory_with_glob(
os.path.normpath(make_snapshot_path(subvolume.path)),
temporary_directory_prefix=BORGMATIC_SNAPSHOT_PREFIX,
)
logger.debug(
f'{log_prefix}: Looking for snapshots to remove in {subvolume_snapshots_glob}{dry_run_label}'
)
for snapshot_path in glob.glob(subvolume_snapshots_glob):
if not os.path.isdir(snapshot_path):
continue
logger.debug(f'{log_prefix}: Deleting Btrfs snapshot {snapshot_path}{dry_run_label}')
if dry_run:
continue
try:
delete_snapshot(btrfs_command, snapshot_path)
except FileNotFoundError:
logger.debug(f'{log_prefix}: Could not find "{btrfs_command}" command')
return
except subprocess.CalledProcessError as error:
logger.debug(f'{log_prefix}: {error}')
return
# Strip off the subvolume path from the end of the snapshot path and then delete the
# resulting directory.
shutil.rmtree(snapshot_path.rsplit(subvolume.path, 1)[0])
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()
@@ -5,15 +5,7 @@ import shutil
logger = logging.getLogger(__name__)
DATA_SOURCE_HOOK_NAMES = (
'bootstrap',
'mariadb_databases',
'mysql_databases',
'mongodb_databases',
'postgresql_databases',
'sqlite_databases',
'zfs',
)
IS_A_HOOK = False
def make_data_source_dump_path(borgmatic_runtime_directory, data_source_hook_name):
+400
View File
@@ -0,0 +1,400 @@
import collections
import glob
import json
import logging
import os
import shutil
import subprocess
import borgmatic.config.paths
import borgmatic.execute
import borgmatic.hooks.data_source.snapshot
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-'
Logical_volume = collections.namedtuple(
'Logical_volume', ('name', 'device_path', 'mount_point', 'contained_source_directories')
)
def get_logical_volumes(lsblk_command, source_directories=None):
'''
Given an lsblk command to run and a sequence of configured source directories, find the
intersection between the current LVM logical volume mount points and the configured borgmatic
source directories. The idea is that these are the requested logical volumes to snapshot.
If source directories is None, include all logical volume mounts points, not just those in
source directories.
Return the result as a sequence of Logical_volume instances.
'''
try:
devices_info = json.loads(
borgmatic.execute.execute_command_and_capture_output(
# Use lsblk instead of lvs here because lvs can't show active mounts.
tuple(lsblk_command.split(' '))
+ (
'--output',
'name,path,mountpoint,type',
'--json',
'--list',
)
)
)
except json.JSONDecodeError as error:
raise ValueError(f'Invalid {lsblk_command} JSON output: {error}')
candidate_source_directories = set(source_directories or ())
try:
return tuple(
Logical_volume(
device['name'], device['path'], device['mountpoint'], contained_source_directories
)
for device in devices_info['blockdevices']
if device['mountpoint'] and device['type'] == 'lvm'
for contained_source_directories in (
borgmatic.hooks.data_source.snapshot.get_contained_directories(
device['mountpoint'], candidate_source_directories
),
)
if not source_directories or contained_source_directories
)
except KeyError as error:
raise ValueError(f'Invalid {lsblk_command} output: Missing key "{error}"')
def snapshot_logical_volume(
lvcreate_command,
snapshot_name,
logical_volume_device,
snapshot_size,
):
'''
Given an lvcreate command to run, a snapshot name, the path to the logical volume device to
snapshot, and a snapshot size string, create a new LVM snapshot.
'''
borgmatic.execute.execute_command(
tuple(lvcreate_command.split(' '))
+ (
'--snapshot',
('--extents' if '%' in snapshot_size else '--size'),
snapshot_size,
'--name',
snapshot_name,
logical_volume_device,
),
output_log_level=logging.DEBUG,
)
def mount_snapshot(mount_command, snapshot_device, snapshot_mount_path): # pragma: no cover
'''
Given a mount command to run, the device path for an existing snapshot, and the path where the
snapshot should be mounted, mount the snapshot as read-only (making any necessary directories
first).
'''
os.makedirs(snapshot_mount_path, mode=0o700, exist_ok=True)
borgmatic.execute.execute_command(
tuple(mount_command.split(' '))
+ (
'-o',
'ro',
snapshot_device,
snapshot_mount_path,
),
output_log_level=logging.DEBUG,
)
DEFAULT_SNAPSHOT_SIZE = '10%ORIGIN'
def dump_data_sources(
hook_config,
config,
log_prefix,
config_paths,
borgmatic_runtime_directory,
source_directories,
dry_run,
):
'''
Given an LVM 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 LVM logical volume mount points listed in the given
source directories. Also update those source directories, replacing logical volume mount points
with corresponding snapshot directories so they get stored in the Borg archive instead. 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 LVM logical volumes{dry_run_label}')
# List logical volumes to get their mount points.
lsblk_command = hook_config.get('lsblk_command', 'lsblk')
requested_logical_volumes = get_logical_volumes(lsblk_command, source_directories)
# Snapshot each logical volume, rewriting source directories to use the snapshot paths.
snapshot_suffix = f'{BORGMATIC_SNAPSHOT_PREFIX}{os.getpid()}'
normalized_runtime_directory = os.path.normpath(borgmatic_runtime_directory)
if not requested_logical_volumes:
logger.warning(f'{log_prefix}: No LVM logical volumes found to snapshot{dry_run_label}')
for logical_volume in requested_logical_volumes:
snapshot_name = f'{logical_volume.name}_{snapshot_suffix}'
logger.debug(
f'{log_prefix}: Creating LVM snapshot {snapshot_name} of {logical_volume.mount_point}{dry_run_label}'
)
if not dry_run:
snapshot_logical_volume(
hook_config.get('lvcreate_command', 'lvcreate'),
snapshot_name,
logical_volume.device_path,
hook_config.get('snapshot_size', DEFAULT_SNAPSHOT_SIZE),
)
# Get the device path for the snapshot we just created.
try:
snapshot = get_snapshots(
hook_config.get('lvs_command', 'lvs'), snapshot_name=snapshot_name
)[0]
except IndexError:
raise ValueError(f'Cannot find LVM snapshot {snapshot_name}')
# Mount the snapshot into a particular named temporary directory so that the snapshot ends
# up in the Borg archive at the "original" logical volume mount point path.
snapshot_mount_path = os.path.join(
normalized_runtime_directory,
'lvm_snapshots',
logical_volume.mount_point.lstrip(os.path.sep),
)
logger.debug(
f'{log_prefix}: Mounting LVM snapshot {snapshot_name} at {snapshot_mount_path}{dry_run_label}'
)
if dry_run:
continue
mount_snapshot(
hook_config.get('mount_command', 'mount'), snapshot.device_path, snapshot_mount_path
)
# Update the path for each contained source directory, so Borg sees it within the
# mounted snapshot.
for source_directory in logical_volume.contained_source_directories:
try:
source_directories.remove(source_directory)
except ValueError:
pass
source_directories.append(
os.path.join(
normalized_runtime_directory,
'lvm_snapshots',
'.', # Borg 1.4+ "slashdot" hack.
source_directory.lstrip(os.path.sep),
)
)
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(
tuple(umount_command.split(' ')) + (snapshot_mount_path,),
output_log_level=logging.DEBUG,
)
def remove_snapshot(lvremove_command, snapshot_device_path): # pragma: no cover
'''
Given an lvremove command to run and the device path of a snapshot, remove it it.
'''
borgmatic.execute.execute_command(
tuple(lvremove_command.split(' '))
+ (
'--force', # Suppress an interactive "are you sure?" type prompt.
snapshot_device_path,
),
output_log_level=logging.DEBUG,
)
Snapshot = collections.namedtuple(
'Snapshot',
('name', 'device_path'),
)
def get_snapshots(lvs_command, snapshot_name=None):
'''
Given an lvs command to run, return all LVM snapshots as a sequence of Snapshot instances.
If a snapshot name is given, filter the results to that snapshot.
'''
try:
snapshot_info = json.loads(
borgmatic.execute.execute_command_and_capture_output(
# Use lvs instead of lsblk here because lsblk can't filter to just snapshots.
tuple(lvs_command.split(' '))
+ (
'--report-format',
'json',
'--options',
'lv_name,lv_path',
'--select',
'lv_attr =~ ^s', # Filter to just snapshots.
)
)
)
except json.JSONDecodeError as error:
raise ValueError(f'Invalid {lvs_command} JSON output: {error}')
try:
return tuple(
Snapshot(snapshot['lv_name'], snapshot['lv_path'])
for snapshot in snapshot_info['report'][0]['lv']
if snapshot_name is None or snapshot['lv_name'] == snapshot_name
)
except IndexError:
raise ValueError(f'Invalid {lvs_command} output: Missing report data')
except KeyError as error:
raise ValueError(f'Invalid {lvs_command} output: Missing key "{error}"')
def remove_data_source_dumps(hook_config, config, log_prefix, borgmatic_runtime_directory, dry_run):
'''
Given an LVM configuration dict, a configuration dict, a log prefix, the borgmatic runtime
directory, and whether this is a dry run, unmount and delete any LVM 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.
try:
logical_volumes = get_logical_volumes(hook_config.get('lsblk_command', 'lsblk'))
except FileNotFoundError as error:
logger.debug(f'{log_prefix}: Could not find "{error.filename}" 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),
),
'lvm_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
for logical_volume in logical_volumes:
snapshot_mount_path = os.path.join(
snapshots_directory, logical_volume.mount_point.lstrip(os.path.sep)
)
if not os.path.isdir(snapshot_mount_path):
continue
# This might fail if the directory is already mounted, but we swallow errors here since
# we'll do another recursive delete below. The point of doing it here is that we don't
# want to try to unmount a non-mounted directory (which *will* fail).
if not dry_run:
shutil.rmtree(snapshot_mount_path, ignore_errors=True)
# If the delete was successful, that means there's nothing to unmount.
if not os.path.isdir(snapshot_mount_path):
continue
logger.debug(
f'{log_prefix}: Unmounting LVM snapshot at {snapshot_mount_path}{dry_run_label}'
)
if dry_run:
continue
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)
# Delete snapshots.
lvremove_command = hook_config.get('lvremove_command', 'lvremove')
try:
snapshots = get_snapshots(hook_config.get('lvs_command', 'lvs'))
except FileNotFoundError as error:
logger.debug(f'{log_prefix}: Could not find "{error.filename}" command')
return
except subprocess.CalledProcessError as error:
logger.debug(f'{log_prefix}: {error}')
return
for snapshot in snapshots:
# Only delete snapshots that borgmatic actually created!
if not snapshot.name.split('_')[-1].startswith(BORGMATIC_SNAPSHOT_PREFIX):
continue
logger.debug(f'{log_prefix}: Deleting LVM snapshot {snapshot.name}{dry_run_label}')
if not dry_run:
remove_snapshot(lvremove_command, snapshot.device_path)
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()
@@ -9,7 +9,7 @@ from borgmatic.execute import (
execute_command_and_capture_output,
execute_command_with_processes,
)
from borgmatic.hooks import dump
from borgmatic.hooks.data_source import dump
logger = logging.getLogger(__name__)
@@ -4,7 +4,7 @@ import shlex
import borgmatic.config.paths
from borgmatic.execute import execute_command, execute_command_with_processes
from borgmatic.hooks import dump
from borgmatic.hooks.data_source import dump
logger = logging.getLogger(__name__)
@@ -9,7 +9,7 @@ from borgmatic.execute import (
execute_command_and_capture_output,
execute_command_with_processes,
)
from borgmatic.hooks import dump
from borgmatic.hooks.data_source import dump
logger = logging.getLogger(__name__)
@@ -11,7 +11,7 @@ from borgmatic.execute import (
execute_command_and_capture_output,
execute_command_with_processes,
)
from borgmatic.hooks import dump
from borgmatic.hooks.data_source import dump
logger = logging.getLogger(__name__)
+30
View File
@@ -0,0 +1,30 @@
import pathlib
IS_A_HOOK = False
def get_contained_directories(parent_directory, candidate_contained_directories):
'''
Given a parent directory and a set of candidate directories potentially inside it, get the
subset of contained directories for which the parent directory is actually the parent, a
grandparent, the very same directory, etc. The idea is if, say, /var/log and /var/lib are
candidate contained directories, but there's a parent directory (logical volume, dataset,
subvolume, etc.) at /var, then /var is what we want to snapshot.
Also mutate the given set of candidate contained directories to remove any actually contained
directories from it. That way, this function can be called multiple times, successively
processing candidate directories until none are left—and avoiding assigning any candidate
directory to more than one parent directory.
'''
if not candidate_contained_directories:
return ()
contained = tuple(
candidate
for candidate in candidate_contained_directories
if pathlib.PurePath(parent_directory) == pathlib.PurePath(candidate)
or pathlib.PurePath(parent_directory) in pathlib.PurePath(candidate).parents
)
candidate_contained_directories -= set(contained)
return contained
@@ -4,7 +4,7 @@ import shlex
import borgmatic.config.paths
from borgmatic.execute import execute_command, execute_command_with_processes
from borgmatic.hooks import dump
from borgmatic.hooks.data_source import dump
logger = logging.getLogger(__name__)
@@ -1,3 +1,4 @@
import collections
import glob
import logging
import os
@@ -6,6 +7,7 @@ import subprocess
import borgmatic.config.paths
import borgmatic.execute
import borgmatic.hooks.data_source.snapshot
logger = logging.getLogger(__name__)
@@ -21,6 +23,13 @@ BORGMATIC_SNAPSHOT_PREFIX = 'borgmatic-'
BORGMATIC_USER_PROPERTY = 'org.torsion.borgmatic:backup'
Dataset = collections.namedtuple(
'Dataset',
('name', 'mount_point', 'auto_backup', 'contained_source_directories'),
defaults=(False, ()),
)
def get_datasets_to_backup(zfs_command, source_directories):
'''
Given a ZFS command to run and a sequence of configured source directories, find the
@@ -29,11 +38,11 @@ def get_datasets_to_backup(zfs_command, source_directories):
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.
Return the result as a sequence of Dataset instances, sorted by mount point.
'''
list_output = borgmatic.execute.execute_command_and_capture_output(
(
zfs_command,
tuple(zfs_command.split(' '))
+ (
'list',
'-H',
'-t',
@@ -42,44 +51,69 @@ def get_datasets_to_backup(zfs_command, source_directories):
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'
# Sort from longest to shortest mount points, so longer mount points get a whack at the
# candidate source directory piñata before their parents do. (Source directories are
# consumed during the second loop below, so no two datasets get the same contained source
# directories.)
datasets = sorted(
(
Dataset(dataset_name, mount_point, (user_property_value == 'auto'), ())
for line in list_output.splitlines()
for (dataset_name, mount_point, user_property_value) in (line.rstrip().split('\t'),)
),
key=lambda dataset: dataset.mount_point,
reverse=True,
)
except ValueError:
raise ValueError('Invalid {zfs_command} list output')
raise ValueError(f'Invalid {zfs_command} list output')
candidate_source_directories = set(source_directories)
return tuple(
sorted(
(
Dataset(
dataset.name,
dataset.mount_point,
dataset.auto_backup,
contained_source_directories,
)
for dataset in datasets
for contained_source_directories in (
(
(dataset.mount_point,)
if dataset.auto_backup
else borgmatic.hooks.data_source.snapshot.get_contained_directories(
dataset.mount_point, candidate_source_directories
)
),
)
if contained_source_directories
),
key=lambda dataset: dataset.mount_point,
)
)
def get_all_datasets(zfs_command):
def get_all_dataset_mount_points(zfs_command):
'''
Given a ZFS command to run, return all ZFS datasets as a sequence of (dataset name, mount point)
pairs.
Given a ZFS command to run, return all ZFS datasets as a sequence of sorted mount points.
'''
list_output = borgmatic.execute.execute_command_and_capture_output(
(
zfs_command,
tuple(zfs_command.split(' '))
+ (
'list',
'-H',
'-t',
'filesystem',
'-o',
'name,mountpoint',
'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')
return tuple(sorted(line.rstrip() for line in list_output.splitlines()))
def snapshot_dataset(zfs_command, full_snapshot_name): # pragma: no cover
@@ -88,8 +122,8 @@ def snapshot_dataset(zfs_command, full_snapshot_name): # pragma: no cover
snapshot.
'''
borgmatic.execute.execute_command(
(
zfs_command,
tuple(zfs_command.split(' '))
+ (
'snapshot',
full_snapshot_name,
),
@@ -104,11 +138,14 @@ def mount_snapshot(mount_command, full_snapshot_name, snapshot_mount_path): # p
first).
'''
os.makedirs(snapshot_mount_path, mode=0o700, exist_ok=True)
borgmatic.execute.execute_command(
(
mount_command,
tuple(mount_command.split(' '))
+ (
'-t',
'zfs',
'-o',
'ro',
full_snapshot_name,
snapshot_mount_path,
),
@@ -131,8 +168,7 @@ def dump_data_sources(
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.
stored in the Borg archive instead. Use the log prefix in any log entries.
Return an empty sequence, since there are no ongoing dump processes from this hook.
@@ -147,37 +183,54 @@ def dump_data_sources(
# Snapshot each dataset, rewriting source directories to use the snapshot paths.
snapshot_name = f'{BORGMATIC_SNAPSHOT_PREFIX}{os.getpid()}'
normalized_runtime_directory = os.path.normpath(borgmatic_runtime_directory)
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 requested_datasets:
logger.warning(f'{log_prefix}: No ZFS datasets found to snapshot{dry_run_label}')
for dataset in requested_datasets:
full_snapshot_name = f'{dataset.name}@{snapshot_name}'
logger.debug(
f'{log_prefix}: Creating ZFS snapshot {full_snapshot_name} of {dataset.mount_point}{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),
snapshot_mount_path = os.path.join(
normalized_runtime_directory,
'zfs_snapshots',
'.', # Borg 1.4+ "slashdot" hack.
mount_point.lstrip(os.path.sep),
dataset.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 dry_run:
continue
mount_snapshot(
hook_config.get('mount_command', 'mount'), full_snapshot_name, snapshot_mount_path
)
for source_directory in dataset.contained_source_directories:
try:
source_directories.remove(source_directory)
except ValueError:
pass
source_directories.append(
os.path.join(
normalized_runtime_directory,
'zfs_snapshots',
'.', # Borg 1.4+ "slashdot" hack.
source_directory.lstrip(os.path.sep),
)
)
if mount_point in source_directories:
source_directories.remove(mount_point)
source_directories.append(snapshot_mount_path_for_borg)
return []
@@ -186,10 +239,7 @@ 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,
),
tuple(umount_command.split(' ')) + (snapshot_mount_path,),
output_log_level=logging.DEBUG,
)
@@ -200,8 +250,8 @@ def destroy_snapshot(zfs_command, full_snapshot_name): # pragma: no cover
it.
'''
borgmatic.execute.execute_command(
(
zfs_command,
tuple(zfs_command.split(' '))
+ (
'destroy',
full_snapshot_name,
),
@@ -215,8 +265,8 @@ def get_all_snapshots(zfs_command):
form "dataset@snapshot".
'''
list_output = borgmatic.execute.execute_command_and_capture_output(
(
zfs_command,
tuple(zfs_command.split(' '))
+ (
'list',
'-H',
'-t',
@@ -242,7 +292,7 @@ def remove_data_source_dumps(hook_config, config, log_prefix, borgmatic_runtime_
zfs_command = hook_config.get('zfs_command', 'zfs')
try:
datasets = get_all_datasets(zfs_command)
dataset_mount_points = get_all_dataset_mount_points(zfs_command)
except FileNotFoundError:
logger.debug(f'{log_prefix}: Could not find "{zfs_command}" command')
return
@@ -265,18 +315,24 @@ def remove_data_source_dumps(hook_config, config, log_prefix, borgmatic_runtime_
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:
# Reversing the sorted datasets ensures that we unmount the longer mount point paths of
# child datasets before the shorter mount point paths of parent datasets.
for mount_point in reversed(dataset_mount_points):
snapshot_mount_path = os.path.join(snapshots_directory, mount_point.lstrip(os.path.sep))
if not os.path.isdir(snapshot_mount_path):
continue
# This might fail if the path is already mounted, but we swallow errors here since we'll
# do another recursive delete 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(snapshot_mount_path, ignore_errors=True)
# If the delete was successful, that means there's nothing to unmount.
if not os.path.isdir(snapshot_mount_path):
continue
logger.debug(
f'{log_prefix}: Unmounting ZFS snapshot at {snapshot_mount_path}{dry_run_label}'
)
+53 -54
View File
@@ -1,75 +1,70 @@
import enum
import importlib
import logging
import pkgutil
from borgmatic.hooks import (
apprise,
bootstrap,
cronhub,
cronitor,
healthchecks,
loki,
mariadb,
mongodb,
mysql,
ntfy,
pagerduty,
postgresql,
pushover,
sqlite,
uptimekuma,
zabbix,
zfs,
)
import borgmatic.hooks.data_source
import borgmatic.hooks.monitoring
logger = logging.getLogger(__name__)
HOOK_NAME_TO_MODULE = {
'apprise': apprise,
'bootstrap': bootstrap,
'cronhub': cronhub,
'cronitor': cronitor,
'healthchecks': healthchecks,
'loki': loki,
'mariadb_databases': mariadb,
'mongodb_databases': mongodb,
'mysql_databases': mysql,
'ntfy': ntfy,
'pagerduty': pagerduty,
'postgresql_databases': postgresql,
'pushover': pushover,
'sqlite_databases': sqlite,
'uptime_kuma': uptimekuma,
'zabbix': zabbix,
'zfs': zfs,
}
class Hook_type(enum.Enum):
DATA_SOURCE = 'data_source'
MONITORING = 'monitoring'
def get_submodule_names(parent_module): # pragma: no cover
'''
Given a parent module, return the names of its direct submodules as a tuple of strings.
'''
return tuple(module_info.name for module_info in pkgutil.iter_modules(parent_module.__path__))
def call_hook(function_name, config, log_prefix, hook_name, *args, **kwargs):
'''
Given a configuration dict and a prefix to use in log entries, call the requested function of
the Python module corresponding to the given hook name. Supply that call with the configuration
for this hook (if any), the log prefix, and any given args and kwargs. Return any return value.
for this hook (if any), the log prefix, and any given args and kwargs. Return the return value
of that call or None if the module in question is not a hook.
Raise ValueError if the hook name is unknown.
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) or {}
hook_config = config.get(hook_name) or config.get(f'{hook_name}_databases') or {}
module_name = hook_name.split('_databases')[0]
try:
module = HOOK_NAME_TO_MODULE[hook_name]
except KeyError:
# Probe for a data source or monitoring hook module corresponding to the hook name.
for parent_module in (borgmatic.hooks.data_source, borgmatic.hooks.monitoring):
if module_name not in get_submodule_names(parent_module):
continue
module = importlib.import_module(f'{parent_module.__name__}.{module_name}')
# If this module is explicitly flagged as not a hook, bail.
if not getattr(module, 'IS_A_HOOK', True):
return None
break
else:
raise ValueError(f'Unknown hook name: {hook_name}')
logger.debug(f'{log_prefix}: Calling {hook_name} hook function {function_name}')
return getattr(module, function_name)(hook_config, config, log_prefix, *args, **kwargs)
def call_hooks(function_name, config, log_prefix, hook_names, *args, **kwargs):
def call_hooks(function_name, config, log_prefix, hook_type, *args, **kwargs):
'''
Given a configuration dict and a prefix to use in log entries, call the requested function of
the Python module corresponding to each given hook name. Supply each call with the configuration
for that hook, the log prefix, and any given args and kwargs. Collect any return values into a
dict from hook name to return value.
the Python module corresponding to each hook of the given hook type (either "data_source" or
"monitoring"). Supply each call with the configuration for that hook, the log prefix, and any
given args and kwargs.
Collect any return values into a dict from module name to return value. Note that the module
name is the name of the hook module itself, which might be different from the hook configuration
option (e.g. "postgresql" for the former vs. "postgresql_databases" for the latter).
If the hook name is not present in the hooks configuration, then don't call the function for it
and omit it from the return values.
@@ -80,22 +75,26 @@ 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 hook_name in config
for hook_name in get_submodule_names(
importlib.import_module(f'borgmatic.hooks.{hook_type.value}')
)
if hook_name in config or f'{hook_name}_databases' in config
}
def call_hooks_even_if_unconfigured(function_name, config, log_prefix, hook_names, *args, **kwargs):
def call_hooks_even_if_unconfigured(function_name, config, log_prefix, hook_type, *args, **kwargs):
'''
Given a configuration dict and a prefix to use in log entries, call the requested function of
the Python module corresponding to each given hook name. Supply each call with the configuration
for that hook, the log prefix, and any given args and kwargs. Collect any return values into a
dict from hook name to return value.
the Python module corresponding to each hook of the given hook type (either "data_source" or
"monitoring"). Supply each call with the configuration for that hook, the log prefix, and any
given args and kwargs. Collect any return values into a dict from hook name to return value.
Raise AttributeError if the function name is not found in the module.
Raise anything else that a called function raises. An error stops calls to subsequent functions.
'''
return {
hook_name: call_hook(function_name, config, log_prefix, hook_name, *args, **kwargs)
for hook_name in hook_names
for hook_name in get_submodule_names(
importlib.import_module(f'borgmatic.hooks.{hook_type.value}')
)
}
-21
View File
@@ -1,21 +0,0 @@
from enum import Enum
MONITOR_HOOK_NAMES = (
'apprise',
'cronhub',
'cronitor',
'healthchecks',
'loki',
'ntfy',
'pagerduty',
'pushover',
'uptime_kuma',
'zabbix',
)
class State(Enum):
START = 1
FINISH = 2
FAIL = 3
LOG = 4
@@ -1,8 +1,8 @@
import logging
import operator
import borgmatic.hooks.logs
import borgmatic.hooks.monitor
import borgmatic.hooks.monitoring.logs
import borgmatic.hooks.monitoring.monitor
logger = logging.getLogger(__name__)
@@ -22,12 +22,12 @@ def initialize_monitor(hook_config, config, config_filename, monitoring_log_leve
logs_size_limit = max(
hook_config.get('logs_size_limit', DEFAULT_LOGS_SIZE_LIMIT_BYTES)
- len(borgmatic.hooks.logs.PAYLOAD_TRUNCATION_INDICATOR),
- len(borgmatic.hooks.monitoring.logs.PAYLOAD_TRUNCATION_INDICATOR),
0,
)
borgmatic.hooks.logs.add_handler(
borgmatic.hooks.logs.Forgetful_buffering_handler(
borgmatic.hooks.monitoring.logs.add_handler(
borgmatic.hooks.monitoring.logs.Forgetful_buffering_handler(
HANDLER_IDENTIFIER, logs_size_limit, monitoring_log_level
)
)
@@ -82,11 +82,13 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
body = state_config.get('body')
if state in (
borgmatic.hooks.monitor.State.FINISH,
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitor.State.LOG,
borgmatic.hooks.monitoring.monitor.State.FINISH,
borgmatic.hooks.monitoring.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.LOG,
):
formatted_logs = borgmatic.hooks.logs.format_buffered_logs_for_payload(HANDLER_IDENTIFIER)
formatted_logs = borgmatic.hooks.monitoring.logs.format_buffered_logs_for_payload(
HANDLER_IDENTIFIER
)
if formatted_logs:
body += f'\n\n{formatted_logs}'
@@ -106,4 +108,4 @@ def destroy_monitor(hook_config, config, config_filename, monitoring_log_level,
Remove the monitor handler that was added to the root logger. This prevents the handler from
getting reused by other instances of this monitor.
'''
borgmatic.hooks.logs.remove_handler(HANDLER_IDENTIFIER)
borgmatic.hooks.monitoring.logs.remove_handler(HANDLER_IDENTIFIER)
@@ -2,7 +2,7 @@ import logging
import requests
from borgmatic.hooks import monitor
from borgmatic.hooks.monitoring import monitor
logger = logging.getLogger(__name__)
@@ -2,7 +2,7 @@ import logging
import requests
from borgmatic.hooks import monitor
from borgmatic.hooks.monitoring import monitor
logger = logging.getLogger(__name__)
@@ -3,8 +3,8 @@ import re
import requests
import borgmatic.hooks.logs
from borgmatic.hooks import monitor
import borgmatic.hooks.monitoring.logs
from borgmatic.hooks.monitoring import monitor
logger = logging.getLogger(__name__)
@@ -30,12 +30,12 @@ def initialize_monitor(hook_config, config, config_filename, monitoring_log_leve
ping_body_limit = max(
hook_config.get('ping_body_limit', DEFAULT_PING_BODY_LIMIT_BYTES)
- len(borgmatic.hooks.logs.PAYLOAD_TRUNCATION_INDICATOR),
- len(borgmatic.hooks.monitoring.logs.PAYLOAD_TRUNCATION_INDICATOR),
0,
)
borgmatic.hooks.logs.add_handler(
borgmatic.hooks.logs.Forgetful_buffering_handler(
borgmatic.hooks.monitoring.logs.add_handler(
borgmatic.hooks.monitoring.logs.Forgetful_buffering_handler(
HANDLER_IDENTIFIER, ping_body_limit, monitoring_log_level
)
)
@@ -78,7 +78,9 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
logger.debug(f'{config_filename}: Using Healthchecks ping URL {ping_url}')
if state in (monitor.State.FINISH, monitor.State.FAIL, monitor.State.LOG):
payload = borgmatic.hooks.logs.format_buffered_logs_for_payload(HANDLER_IDENTIFIER)
payload = borgmatic.hooks.monitoring.logs.format_buffered_logs_for_payload(
HANDLER_IDENTIFIER
)
else:
payload = ''
@@ -99,4 +101,4 @@ def destroy_monitor(hook_config, config, config_filename, monitoring_log_level,
Remove the monitor handler that was added to the root logger. This prevents the handler from
getting reused by other instances of this monitor.
'''
borgmatic.hooks.logs.remove_handler(HANDLER_IDENTIFIER)
borgmatic.hooks.monitoring.logs.remove_handler(HANDLER_IDENTIFIER)
@@ -1,5 +1,6 @@
import logging
IS_A_HOOK = False
PAYLOAD_TRUNCATION_INDICATOR = '...\n'
@@ -6,7 +6,7 @@ import time
import requests
from borgmatic.hooks import monitor
from borgmatic.hooks.monitoring import monitor
logger = logging.getLogger(__name__)
+10
View File
@@ -0,0 +1,10 @@
import enum
IS_A_HOOK = False
class State(enum.Enum):
START = 1
FINISH = 2
FAIL = 3
LOG = 4
@@ -5,7 +5,7 @@ import platform
import requests
from borgmatic.hooks import monitor
from borgmatic.hooks.monitoring import monitor
logger = logging.getLogger(__name__)
+181 -7
View File
@@ -52,7 +52,7 @@ feedback](https://torsion.org/borgmatic/#issues) you have on this feature.
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`.
`source_directories` option.
* 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
@@ -62,17 +62,27 @@ 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
During a backup, borgmatic automatically snapshots these discovered datasets
(non-recursively), 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
and includes the snapshotted files in the paths 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.
dataset is mounted at `/var/dataset`, then the snapshotted files will appear
in an archive at `/var/dataset` as well—even if borgmatic has to mount the
snapshot somewhere in `/run/user/1000/borgmatic/zfs_snapshots/` to perform the
backup.
<span class="minilink minilink-addedin">New in version 1.9.4</span> borgmatic
is smart enough to look at the parent (and grandparent, etc.) directories of
each of your `source_directories` to discover any datasets. For instance,
let's say you add `/var/log` and `/var/lib` to your source directories, but
`/var` is a dataset. borgmatic will discover that and snapshot `/var`
accordingly. This also works even with nested datasets; borgmatic selects
the dataset that's the "closest" parent to your source directories.
<span class="minilink minilink-addedin">With Borg version 1.2 and
earlier</span>Snapshotted files are instead stored at a path dependent on the
@@ -88,3 +98,167 @@ 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.
### Btrfs
<span class="minilink minilink-addedin">New in version 1.9.4</span> <span
class="minilink minilink-addedin">Beta feature</span> borgmatic supports taking
snapshots with the [Btrfs filesystem](https://btrfs.readthedocs.io/) and sending
those snapshots to Borg for backup.
To use this feature, first you need one or more Btrfs subvolumes on mounted
filesystems. Then, enable Btrfs within borgmatic by adding the following line to
your configuration file:
```yaml
btrfs:
```
No other options are necessary to enable Btrfs support, but if desired you can
override some of the commands used by the Btrfs hook. For instance:
```yaml
btrfs:
btrfs_command: /usr/local/bin/btrfs
findmnt_command: /usr/local/bin/findmnt
```
As long as the Btrfs 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.
#### Subvolume discovery
For any subvolume you'd like backed up, add its path to borgmatic's
`source_directories` option. During a backup, borgmatic snapshots these
subvolumes (non-recursively) and includes the snapshotted files in the paths
sent to Borg. borgmatic is also responsible for cleaning up (deleting) these
snapshots after a backup completes.
borgmatic is smart enough to look at the parent (and grandparent, etc.)
directories of each of your `source_directories` to discover any subvolumes.
For instance, let's say you add `/var/log` and `/var/lib` to your source
directories, but `/var` is a subvolume. borgmatic will discover that and
snapshot `/var` accordingly. This also works even with nested subvolumes;
borgmatic selects the subvolume that's the "closest" parent to your source
directories.
Additionally, borgmatic rewrites the snapshot file paths so that they appear
at their original subvolume locations in a Borg archive. For instance, if your
subvolume exists at `/var/subvolume`, then the snapshotted files will appear
in an archive at `/var/subvolume` as well—even if borgmatic has to mount the
snapshot somewhere in `/var/subvolume/.borgmatic-snapshot-1234/` to perform
the backup.
<span class="minilink minilink-addedin">With Borg version 1.2 and
earlier</span>Snapshotted files are instead stored at a path dependent on the
temporary snapshot directory in use at the time the archive was created, as Borg
1.2 and earlier do not support path rewriting.
#### Extract a subvolume
Subvolume 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.
### LVM
<span class="minilink minilink-addedin">New in version 1.9.4</span> <span
class="minilink minilink-addedin">Beta feature</span> borgmatic supports
taking snapshots with [LVM](https://sourceware.org/lvm2/) (Linux Logical
Volume Manager) and sending those snapshots to Borg for backup. LVM isn't
itself a filesystem, but it can take snapshots at the layer right below your
filesystem.
To use this feature, first you need one or more mounted LVM logical volumes.
Then, enable LVM within borgmatic by adding the following line to your
configuration file:
```yaml
lvm:
```
No other options are necessary to enable LVM support, but if desired you can
override some of the options used by the LVM hook. For instance:
```yaml
lvm:
snapshot_size: 5GB # See below for details.
lvcreate_command: /usr/local/bin/lvcreate
lvremove_command: /usr/local/bin/lvremove
lvs_command: /usr/local/bin/lvs
lsbrk_command: /usr/local/bin/lsbrk
mount_command: /usr/local/bin/mount
umount_command: /usr/local/bin/umount
```
As long as the LVM 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.
#### Snapshot size
The `snapshot_size` option is the size to allocate for each snapshot taken,
including the units to use for that size. While borgmatic's snapshots
themselves are read-only and don't change during backups, the logical volume
being snapshotted *can* change—therefore requiring additional snapshot storage
since LVM snapshots are copy-on-write. And if the configured snapshot size is
too small (and LVM isn't configured to grow snapshots automatically), then the
snapshots will fail to allocate enough space, resulting in a broken backup.
If not specified, the `snapshot_size` option defaults to `10%ORIGIN`, which
means 10% of the size of logical volume being snapshotted. See the [`lvcreate
--size` and `--extents`
documentation](https://www.man7.org/linux/man-pages/man8/lvcreate.8.html) for
more information about possible values here. (Under the hood, borgmatic uses
`lvcreate --extents` if the `snapshot_size` is a percentage value, and
`lvcreate --size` otherwise.)
#### Logical volume discovery
For any logical volume you'd like backed up, add its mount point to
borgmatic's `source_directories` option.
During a backup, borgmatic automatically snapshots these discovered logical
volumes (non-recursively), 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 paths sent to Borg. borgmatic is
also responsible for cleaning up (deleting) these snapshots after a backup
completes.
borgmatic is smart enough to look at the parent (and grandparent, etc.)
directories of each of your `source_directories` to discover any logical
volumes. For instance, let's say you add `/var/log` and `/var/lib` to your
source directories, but `/var` is a logical volume. borgmatic will discover
that and snapshot `/var` accordingly.
Additionally, borgmatic rewrites the snapshot file paths so that they appear
at their original logical volume locations in a Borg archive. For instance, if
your logical volume is mounted at `/var/lvolume`, then the snapshotted files
will appear in an archive at `/var/lvolume` as well—even if borgmatic has to
mount the snapshot somewhere in `/run/user/1000/borgmatic/lvm_snapshots/` to
perform the backup.
<span class="minilink minilink-addedin">With Borg version 1.2 and
earlier</span>Snapshotted files are instead stored at a path dependent on the
[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 logical volume
Logical volume 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.
+4 -2
View File
@@ -16,8 +16,10 @@ you get started. Starting at the top level, we have:
* [actions](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/actions): borgmatic-specific logic for running each action (create, list, check, etc.).
* [borg](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/borg): Lower-level code that's responsible for interacting with Borg to run each action.
* [commands](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/commands): Looking to add a new flag or action? Start here. This contains borgmatic's entry point, argument parsing, and shell completion.
* [config](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/config): Code responsible for loading, normalizing, and validating borgmatic's configuration.
* [hooks](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/hooks): Looking to add a new database or monitoring integration? Start here.
* [config](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/config): Code responsible for loading, normalizing, and validating borgmatic's configuration. Interested in adding a new configuration option? Check out `schema.yaml` here.
* [hooks](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/hooks): Looking to add a new database, filesystem, or monitoring integration? Start here.
* [data_source](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/hooks/data_source): Database and filesystem hooks—anything that produces data or files to go into a backup archive.
* [monitoring](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/hooks/monitoring): Monitoring hooks—integrations with third-party or self-hosted monitoring services.
* [docs](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/docs): How-to and reference documentation, including the document you're reading now.
* [sample](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/sample): Example configurations for cron and systemd.
* [scripts](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/scripts): Dev-facing scripts for things like building documentation and running end-to-end tests.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "borgmatic"
version = "1.9.3"
version = "1.9.4"
authors = [
{ name="Dan Helfman", email="witten@torsion.org" },
]
+90
View File
@@ -0,0 +1,90 @@
import argparse
import json
import os
import shutil
import sys
def parse_arguments(*unparsed_arguments):
global_parser = argparse.ArgumentParser(add_help=False)
action_parsers = global_parser.add_subparsers(dest='action')
subvolume_parser = action_parsers.add_parser('subvolume')
subvolume_subparser = subvolume_parser.add_subparsers(dest='subaction')
list_parser = subvolume_subparser.add_parser('list')
list_parser.add_argument('-s', dest='snapshots_only', action='store_true')
list_parser.add_argument('subvolume_path')
snapshot_parser = subvolume_subparser.add_parser('snapshot')
snapshot_parser.add_argument('-r', dest='read_only', action='store_true')
snapshot_parser.add_argument('subvolume_path')
snapshot_parser.add_argument('snapshot_path')
delete_parser = subvolume_subparser.add_parser('delete')
delete_parser.add_argument('snapshot_path')
return global_parser.parse_args(unparsed_arguments)
BUILTIN_SUBVOLUME_LIST_LINES = (
'261 gen 29 top level 5 path sub',
'262 gen 29 top level 5 path other',
)
SUBVOLUME_LIST_LINE_PREFIX = '263 gen 29 top level 5 path '
def load_snapshots():
try:
return json.load(open('/tmp/fake_btrfs.json'))
except FileNotFoundError:
return []
def save_snapshots(snapshot_paths):
json.dump(snapshot_paths, open('/tmp/fake_btrfs.json', 'w'))
def print_subvolume_list(arguments, snapshot_paths):
assert arguments.subvolume_path == '/mnt/subvolume'
if not arguments.snapshots_only:
for line in BUILTIN_SUBVOLUME_LIST_LINES:
print(line)
for snapshot_path in snapshot_paths:
print(
SUBVOLUME_LIST_LINE_PREFIX
+ snapshot_path[snapshot_path.index('.borgmatic-snapshot-') :]
)
def main():
arguments = parse_arguments(*sys.argv[1:])
snapshot_paths = load_snapshots()
if arguments.subaction == 'list':
print_subvolume_list(arguments, snapshot_paths)
elif arguments.subaction == 'snapshot':
snapshot_paths.append(arguments.snapshot_path)
save_snapshots(snapshot_paths)
subdirectory = os.path.join(arguments.snapshot_path, 'subdir')
os.makedirs(subdirectory, mode=0o700, exist_ok=True)
test_file = open(os.path.join(subdirectory, 'file.txt'), 'w')
test_file.write('contents')
test_file.close()
elif arguments.subaction == 'delete':
subdirectory = os.path.join(arguments.snapshot_path, 'subdir')
shutil.rmtree(subdirectory)
snapshot_paths = [
snapshot_path
for snapshot_path in snapshot_paths
if snapshot_path.endswith('/' + arguments.snapshot_path)
]
save_snapshots(snapshot_paths)
if __name__ == '__main__':
main()
+33
View File
@@ -0,0 +1,33 @@
import argparse
import sys
def parse_arguments(*unparsed_arguments):
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('-n', dest='headings', action='store_false', default=True)
parser.add_argument('-t', dest='type')
return parser.parse_args(unparsed_arguments)
BUILTIN_FILESYSTEM_MOUNT_LINES = (
'/mnt/subvolume /dev/loop1 btrfs rw,relatime,ssd,space_cache=v2,subvolid=5,subvol=/',
)
def print_filesystem_mounts(arguments):
for line in BUILTIN_FILESYSTEM_MOUNT_LINES:
print(line)
def main():
arguments = parse_arguments(*sys.argv[1:])
assert not arguments.headings
assert arguments.type == 'btrfs'
print_filesystem_mounts(arguments)
if __name__ == '__main__':
main()
+70
View File
@@ -0,0 +1,70 @@
import argparse
import json
import sys
def parse_arguments(*unparsed_arguments):
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--output', required=True)
parser.add_argument('--json', action='store_true', required=True)
parser.add_argument('--list', action='store_true', required=True)
return parser.parse_args(unparsed_arguments)
BUILTIN_BLOCK_DEVICES = {
'blockdevices': [
{'name': 'loop0', 'path': '/dev/loop0', 'mountpoint': None, 'type': 'loop'},
{'name': 'cryptroot', 'path': '/dev/mapper/cryptroot', 'mountpoint': '/', 'type': 'crypt'},
{
'name': 'vgroup-lvolume',
'path': '/dev/mapper/vgroup-lvolume',
'mountpoint': '/mnt/lvolume',
'type': 'lvm',
},
{
'name': 'vgroup-lvolume-real',
'path': '/dev/mapper/vgroup-lvolume-real',
'mountpoint': None,
'type': 'lvm',
},
]
}
def load_snapshots():
try:
return json.load(open('/tmp/fake_lvm.json'))
except FileNotFoundError:
return []
def print_logical_volumes_json(arguments, snapshots):
data = dict(BUILTIN_BLOCK_DEVICES)
for snapshot in snapshots:
data['blockdevices'].extend(
{
'name': snapshot['lv_name'],
'path': snapshot['lv_path'],
'mountpoint': None,
'type': 'lvm',
}
for snapshot in snapshots
)
print(json.dumps(data))
def main():
arguments = parse_arguments(*sys.argv[1:])
snapshots = load_snapshots()
assert arguments.output == 'name,path,mountpoint,type'
print_logical_volumes_json(arguments, snapshots)
if __name__ == '__main__':
main()
@@ -0,0 +1,43 @@
import argparse
import json
import sys
def parse_arguments(*unparsed_arguments):
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--snapshot', action='store_true', required=True)
parser.add_argument('--extents')
parser.add_argument('--size')
parser.add_argument('--name', dest='snapshot_name', required=True)
parser.add_argument('logical_volume_device')
return parser.parse_args(unparsed_arguments)
def load_snapshots():
try:
return json.load(open('/tmp/fake_lvm.json'))
except FileNotFoundError:
return []
def save_snapshots(snapshots):
json.dump(snapshots, open('/tmp/fake_lvm.json', 'w'))
def main():
arguments = parse_arguments(*sys.argv[1:])
snapshots = load_snapshots()
assert arguments.extents or arguments.size
snapshots.append(
{'lv_name': arguments.snapshot_name, 'lv_path': f'/dev/vgroup/{arguments.snapshot_name}'},
)
save_snapshots(snapshots)
if __name__ == '__main__':
main()
@@ -0,0 +1,39 @@
import argparse
import json
import sys
def parse_arguments(*unparsed_arguments):
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--force', action='store_true', required=True)
parser.add_argument('snapshot_device')
return parser.parse_args(unparsed_arguments)
def load_snapshots():
try:
return json.load(open('/tmp/fake_lvm.json'))
except FileNotFoundError:
return []
def save_snapshots(snapshots):
json.dump(snapshots, open('/tmp/fake_lvm.json', 'w'))
def main():
arguments = parse_arguments(*sys.argv[1:])
snapshots = [
snapshot
for snapshot in load_snapshots()
if snapshot['lv_path'] != arguments.snapshot_device
]
save_snapshots(snapshots)
if __name__ == '__main__':
main()
+50
View File
@@ -0,0 +1,50 @@
import argparse
import json
import sys
def parse_arguments(*unparsed_arguments):
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--report-format', required=True)
parser.add_argument('--options', required=True)
parser.add_argument('--select', required=True)
return parser.parse_args(unparsed_arguments)
def load_snapshots():
try:
return json.load(open('/tmp/fake_lvm.json'))
except FileNotFoundError:
return []
def print_snapshots_json(arguments, snapshots):
assert arguments.report_format == 'json'
assert arguments.options == 'lv_name,lv_path'
assert arguments.select == 'lv_attr =~ ^s'
print(
json.dumps(
{
'report': [
{
'lv': snapshots,
}
],
'log': [],
}
)
)
def main():
arguments = parse_arguments(*sys.argv[1:])
snapshots = load_snapshots()
print_snapshots_json(arguments, snapshots)
if __name__ == '__main__':
main()
+29
View File
@@ -0,0 +1,29 @@
import argparse
import os
import sys
def parse_arguments(*unparsed_arguments):
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('-t', dest='type')
parser.add_argument('-o', dest='options')
parser.add_argument('snapshot_name')
parser.add_argument('mount_point')
return parser.parse_args(unparsed_arguments)
def main():
arguments = parse_arguments(*sys.argv[1:])
assert arguments.options == 'ro'
subdirectory = os.path.join(arguments.mount_point, 'subdir')
os.mkdir(subdirectory)
test_file = open(os.path.join(subdirectory, 'file.txt'), 'w')
test_file.write('contents')
test_file.close()
if __name__ == '__main__':
main()
+22
View File
@@ -0,0 +1,22 @@
import argparse
import os
import shutil
import sys
def parse_arguments(*unparsed_arguments):
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('mount_point')
return parser.parse_args(unparsed_arguments)
def main():
arguments = parse_arguments(*sys.argv[1:])
subdirectory = os.path.join(arguments.mount_point, 'subdir')
shutil.rmtree(subdirectory)
if __name__ == '__main__':
main()
+92
View File
@@ -0,0 +1,92 @@
import argparse
import json
import sys
def parse_arguments(*unparsed_arguments):
global_parser = argparse.ArgumentParser(add_help=False)
action_parsers = global_parser.add_subparsers(dest='action')
list_parser = action_parsers.add_parser('list')
list_parser.add_argument('-H', dest='header', action='store_false', default=True)
list_parser.add_argument('-t', dest='type', default='filesystem')
list_parser.add_argument('-o', dest='properties', default='name,used,avail,refer,mountpoint')
snapshot_parser = action_parsers.add_parser('snapshot')
snapshot_parser.add_argument('name')
destroy_parser = action_parsers.add_parser('destroy')
destroy_parser.add_argument('name')
return global_parser.parse_args(unparsed_arguments)
BUILTIN_DATASETS = (
{
'name': 'pool',
'used': '256K',
'avail': '23.7M',
'refer': '25K',
'mountpoint': '/pool',
},
{
'name': 'pool/dataset',
'used': '256K',
'avail': '23.7M',
'refer': '25K',
'mountpoint': '/pool/dataset',
},
)
def load_snapshots():
try:
return json.load(open('/tmp/fake_zfs.json'))
except FileNotFoundError:
return []
def save_snapshots(snapshots):
json.dump(snapshots, open('/tmp/fake_zfs.json', 'w'))
def print_dataset_list(arguments, datasets, snapshots):
properties = arguments.properties.split(',')
data = (
(tuple(property_name.upper() for property_name in properties),) if arguments.header else ()
) + tuple(
tuple(dataset.get(property_name, '-') for property_name in properties)
for dataset in (snapshots if arguments.type == 'snapshot' else datasets)
)
if not data:
return
for data_row in data:
print('\t'.join(data_row))
def main():
arguments = parse_arguments(*sys.argv[1:])
snapshots = load_snapshots()
if arguments.action == 'list':
print_dataset_list(arguments, BUILTIN_DATASETS, snapshots)
elif arguments.action == 'snapshot':
snapshots.append(
{
'name': arguments.name,
'used': '0B',
'avail': '-',
'refer': '25K',
'mountpoint': '-',
},
)
save_snapshots(snapshots)
elif arguments.action == 'destroy':
snapshots = [snapshot for snapshot in snapshots if snapshot['name'] != arguments.name]
save_snapshots(snapshots)
if __name__ == '__main__':
main()
+63
View File
@@ -0,0 +1,63 @@
import os
import shutil
import subprocess
import sys
import tempfile
def generate_configuration(config_path, repository_path):
'''
Generate borgmatic configuration into a file at the config path, and update the defaults so as
to work for testing (including injecting the given repository path and tacking on an encryption
passphrase).
'''
subprocess.check_call(f'borgmatic config generate --destination {config_path}'.split(' '))
config = (
open(config_path)
.read()
.replace('ssh://user@backupserver/./sourcehostname.borg', repository_path)
.replace('- path: /mnt/backup', '')
.replace('label: local', '')
.replace('- /home', f'- {config_path}')
.replace('- /etc', '- /mnt/subvolume/subdir')
.replace('- /var/log/syslog*', '')
+ 'encryption_passphrase: "test"\n'
+ 'btrfs:\n'
+ ' btrfs_command: python3 /app/tests/end-to-end/commands/fake_btrfs.py\n'
+ ' findmnt_command: python3 /app/tests/end-to-end/commands/fake_findmnt.py\n'
)
config_file = open(config_path, 'w')
config_file.write(config)
config_file.close()
def test_btrfs_create_and_list():
temporary_directory = tempfile.mkdtemp()
repository_path = os.path.join(temporary_directory, 'test.borg')
try:
config_path = os.path.join(temporary_directory, 'test.yaml')
generate_configuration(config_path, repository_path)
subprocess.check_call(
f'borgmatic -v 2 --config {config_path} repo-create --encryption repokey'.split(' ')
)
# Run a create action to exercise Btrfs snapshotting and backup.
subprocess.check_call(f'borgmatic --config {config_path} create'.split(' '))
# List the resulting archive and assert that the snapshotted files are there.
output = subprocess.check_output(
f'borgmatic --config {config_path} list --archive latest'.split(' ')
).decode(sys.stdout.encoding)
assert 'mnt/subvolume/subdir/file.txt' in output
# Assert that the snapshot has been deleted.
assert not subprocess.check_output(
'python3 /app/tests/end-to-end/commands/fake_btrfs.py subvolume list -s /mnt/subvolume'.split(
' '
)
)
finally:
shutil.rmtree(temporary_directory)
+71
View File
@@ -0,0 +1,71 @@
import json
import os
import shutil
import subprocess
import sys
import tempfile
def generate_configuration(config_path, repository_path):
'''
Generate borgmatic configuration into a file at the config path, and update the defaults so as
to work for testing (including injecting the given repository path and tacking on an encryption
passphrase).
'''
subprocess.check_call(f'borgmatic config generate --destination {config_path}'.split(' '))
config = (
open(config_path)
.read()
.replace('ssh://user@backupserver/./sourcehostname.borg', repository_path)
.replace('- path: /mnt/backup', '')
.replace('label: local', '')
.replace('- /home', f'- {config_path}')
.replace('- /etc', '- /mnt/lvolume/subdir')
.replace('- /var/log/syslog*', '')
+ 'encryption_passphrase: "test"\n'
+ 'lvm:\n'
+ ' lsblk_command: python3 /app/tests/end-to-end/commands/fake_lsblk.py\n'
+ ' lvcreate_command: python3 /app/tests/end-to-end/commands/fake_lvcreate.py\n'
+ ' lvremove_command: python3 /app/tests/end-to-end/commands/fake_lvremove.py\n'
+ ' lvs_command: python3 /app/tests/end-to-end/commands/fake_lvs.py\n'
+ ' mount_command: python3 /app/tests/end-to-end/commands/fake_mount.py\n'
+ ' umount_command: python3 /app/tests/end-to-end/commands/fake_umount.py\n'
)
config_file = open(config_path, 'w')
config_file.write(config)
config_file.close()
def test_lvm_create_and_list():
temporary_directory = tempfile.mkdtemp()
repository_path = os.path.join(temporary_directory, 'test.borg')
try:
config_path = os.path.join(temporary_directory, 'test.yaml')
generate_configuration(config_path, repository_path)
subprocess.check_call(
f'borgmatic -v 2 --config {config_path} repo-create --encryption repokey'.split(' ')
)
# Run a create action to exercise LVM snapshotting and backup.
subprocess.check_call(f'borgmatic --config {config_path} create'.split(' '))
# List the resulting archive and assert that the snapshotted files are there.
output = subprocess.check_output(
f'borgmatic --config {config_path} list --archive latest'.split(' ')
).decode(sys.stdout.encoding)
assert 'mnt/lvolume/subdir/file.txt' in output
# Assert that the snapshot has been deleted.
assert not json.loads(
subprocess.check_output(
'python3 /app/tests/end-to-end/commands/fake_lvs.py --report-format json --options lv_name,lv_path --select'.split(
' '
)
+ ['lv_attr =~ ^s']
)
)['report'][0]['lv']
finally:
shutil.rmtree(temporary_directory)
+62
View File
@@ -0,0 +1,62 @@
import os
import shutil
import subprocess
import sys
import tempfile
def generate_configuration(config_path, repository_path):
'''
Generate borgmatic configuration into a file at the config path, and update the defaults so as
to work for testing (including injecting the given repository path and tacking on an encryption
passphrase).
'''
subprocess.check_call(f'borgmatic config generate --destination {config_path}'.split(' '))
config = (
open(config_path)
.read()
.replace('ssh://user@backupserver/./sourcehostname.borg', repository_path)
.replace('- path: /mnt/backup', '')
.replace('label: local', '')
.replace('- /home', f'- {config_path}')
.replace('- /etc', '- /pool/dataset/subdir')
.replace('- /var/log/syslog*', '')
+ 'encryption_passphrase: "test"\n'
+ 'zfs:\n'
+ ' zfs_command: python3 /app/tests/end-to-end/commands/fake_zfs.py\n'
+ ' mount_command: python3 /app/tests/end-to-end/commands/fake_mount.py\n'
+ ' umount_command: python3 /app/tests/end-to-end/commands/fake_umount.py'
)
config_file = open(config_path, 'w')
config_file.write(config)
config_file.close()
def test_zfs_create_and_list():
temporary_directory = tempfile.mkdtemp()
repository_path = os.path.join(temporary_directory, 'test.borg')
try:
config_path = os.path.join(temporary_directory, 'test.yaml')
generate_configuration(config_path, repository_path)
subprocess.check_call(
f'borgmatic -v 2 --config {config_path} repo-create --encryption repokey'.split(' ')
)
# Run a create action to exercise ZFS snapshotting and backup.
subprocess.check_call(f'borgmatic --config {config_path} create'.split(' '))
# List the resulting archive and assert that the snapshotted files are there.
output = subprocess.check_output(
f'borgmatic --config {config_path} list --archive latest'.split(' ')
).decode(sys.stdout.encoding)
assert 'pool/dataset/subdir/file.txt' in output
# Assert that the snapshot has been deleted.
assert not subprocess.check_output(
'python3 /app/tests/end-to-end/commands/fake_zfs.py list -H -t snapshot'.split(' ')
)
finally:
shutil.rmtree(temporary_directory)
View File
@@ -2,14 +2,14 @@ import logging
from flexmock import flexmock
from borgmatic.hooks import apprise as module
from borgmatic.hooks.monitoring import apprise as module
def test_destroy_monitor_removes_apprise_handler():
logger = logging.getLogger()
original_handlers = list(logger.handlers)
module.borgmatic.hooks.logs.add_handler(
module.borgmatic.hooks.logs.Forgetful_buffering_handler(
module.borgmatic.hooks.monitoring.logs.add_handler(
module.borgmatic.hooks.monitoring.logs.Forgetful_buffering_handler(
identifier=module.HANDLER_IDENTIFIER, byte_capacity=100, log_level=1
)
)
@@ -2,14 +2,14 @@ import logging
from flexmock import flexmock
from borgmatic.hooks import healthchecks as module
from borgmatic.hooks.monitoring import healthchecks as module
def test_destroy_monitor_removes_healthchecks_handler():
logger = logging.getLogger()
original_handlers = list(logger.handlers)
module.borgmatic.hooks.logs.add_handler(
module.borgmatic.hooks.logs.Forgetful_buffering_handler(
module.borgmatic.hooks.monitoring.logs.add_handler(
module.borgmatic.hooks.monitoring.logs.Forgetful_buffering_handler(
identifier=module.HANDLER_IDENTIFIER, byte_capacity=100, log_level=1
)
)
@@ -3,7 +3,7 @@ import platform
from flexmock import flexmock
from borgmatic.hooks import loki as module
from borgmatic.hooks.monitoring import loki as module
def test_initialize_monitor_replaces_labels():
+24 -24
View File
@@ -90,7 +90,7 @@ def test_restore_single_data_source_extracts_and_restores_single_file_dump():
'make_data_source_dump_patterns', object, object, object, object, object
).and_return({'postgresql': flexmock()})
flexmock(module.tempfile).should_receive('mkdtemp').never()
flexmock(module.borgmatic.hooks.dump).should_receive(
flexmock(module.borgmatic.hooks.data_source.dump).should_receive(
'convert_glob_patterns_to_borg_pattern'
).and_return(flexmock())
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
@@ -98,11 +98,11 @@ def test_restore_single_data_source_extracts_and_restores_single_file_dump():
).once()
flexmock(module).should_receive('strip_path_prefix_from_extracted_dump_destination').never()
flexmock(module.shutil).should_receive('rmtree').never()
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').with_args(
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').with_args(
function_name='restore_data_source_dump',
config=object,
log_prefix=object,
hook_names=object,
hook_name=object,
data_source=object,
dry_run=object,
extract_process=object,
@@ -132,7 +132,7 @@ def test_restore_single_data_source_extracts_and_restores_directory_dump():
flexmock(module.tempfile).should_receive('mkdtemp').once().and_return(
'/run/user/0/borgmatic/tmp1234'
)
flexmock(module.borgmatic.hooks.dump).should_receive(
flexmock(module.borgmatic.hooks.data_source.dump).should_receive(
'convert_glob_patterns_to_borg_pattern'
).and_return(flexmock())
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
@@ -140,11 +140,11 @@ def test_restore_single_data_source_extracts_and_restores_directory_dump():
).once()
flexmock(module).should_receive('strip_path_prefix_from_extracted_dump_destination').once()
flexmock(module.shutil).should_receive('rmtree').once()
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').with_args(
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').with_args(
function_name='restore_data_source_dump',
config=object,
log_prefix=object,
hook_names=object,
hook_name=object,
data_source=object,
dry_run=object,
extract_process=object,
@@ -174,7 +174,7 @@ def test_restore_single_data_source_with_directory_dump_error_cleans_up_temporar
flexmock(module.tempfile).should_receive('mkdtemp').once().and_return(
'/run/user/0/borgmatic/tmp1234'
)
flexmock(module.borgmatic.hooks.dump).should_receive(
flexmock(module.borgmatic.hooks.data_source.dump).should_receive(
'convert_glob_patterns_to_borg_pattern'
).and_return(flexmock())
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_raise(
@@ -182,11 +182,11 @@ def test_restore_single_data_source_with_directory_dump_error_cleans_up_temporar
).once()
flexmock(module).should_receive('strip_path_prefix_from_extracted_dump_destination').never()
flexmock(module.shutil).should_receive('rmtree').once()
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').with_args(
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').with_args(
function_name='restore_data_source_dump',
config=object,
log_prefix=object,
hook_names=object,
hook_name=object,
data_source=object,
dry_run=object,
extract_process=object,
@@ -215,7 +215,7 @@ def test_restore_single_data_source_with_directory_dump_and_dry_run_skips_direct
'make_data_source_dump_patterns', object, object, object, object, object
).and_return({'postgresql': flexmock()})
flexmock(module.tempfile).should_receive('mkdtemp').once().and_return('/run/borgmatic/tmp1234')
flexmock(module.borgmatic.hooks.dump).should_receive(
flexmock(module.borgmatic.hooks.data_source.dump).should_receive(
'convert_glob_patterns_to_borg_pattern'
).and_return(flexmock())
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
@@ -223,11 +223,11 @@ def test_restore_single_data_source_with_directory_dump_and_dry_run_skips_direct
).once()
flexmock(module).should_receive('strip_path_prefix_from_extracted_dump_destination').never()
flexmock(module.shutil).should_receive('rmtree').never()
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').with_args(
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').with_args(
function_name='restore_data_source_dump',
config=object,
log_prefix=object,
hook_names=object,
hook_name=object,
data_source=object,
dry_run=object,
extract_process=object,
@@ -254,9 +254,9 @@ 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.hooks.dump).should_receive('make_data_source_dump_path').and_return(
''
)
flexmock(module.borgmatic.hooks.data_source.dump).should_receive(
'make_data_source_dump_path'
).and_return('')
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
[
'borgmatic/postgresql_databases/localhost/foo',
@@ -286,9 +286,9 @@ 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.hooks.dump).should_receive('make_data_source_dump_path').and_return(
''
)
flexmock(module.borgmatic.hooks.data_source.dump).should_receive(
'make_data_source_dump_path'
).and_return('')
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
[
'borgmatic/postgresql_databases/localhost/foo',
@@ -319,9 +319,9 @@ 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.hooks.dump).should_receive('make_data_source_dump_path').and_return(
''
)
flexmock(module.borgmatic.hooks.data_source.dump).should_receive(
'make_data_source_dump_path'
).and_return('')
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
[
'borgmatic/postgresql_databases/localhost/foo/table1',
@@ -349,9 +349,9 @@ 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.hooks.dump).should_receive('make_data_source_dump_path').and_return(
''
)
flexmock(module.borgmatic.hooks.data_source.dump).should_receive(
'make_data_source_dump_path'
).and_return('')
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
[
'borgmatic/postgresql_databases/localhost/foo',
+16
View File
@@ -45,6 +45,22 @@ def test_replace_temporary_subdirectory_with_glob_transforms_path():
)
def test_replace_temporary_subdirectory_with_glob_passes_through_non_matching_path():
assert (
module.replace_temporary_subdirectory_with_glob('/tmp/foo-aet8kn93/borgmatic')
== '/tmp/foo-aet8kn93/borgmatic'
)
def test_replace_temporary_subdirectory_with_glob_uses_custom_temporary_directory_prefix():
assert (
module.replace_temporary_subdirectory_with_glob(
'/tmp/.borgmatic-aet8kn93/borgmatic', temporary_directory_prefix='.borgmatic-'
)
== '/tmp/.borgmatic-*/borgmatic'
)
def test_runtime_directory_uses_config_option():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os).should_receive('makedirs')
@@ -2,7 +2,7 @@ import sys
from flexmock import flexmock
from borgmatic.hooks import bootstrap as module
from borgmatic.hooks.data_source import bootstrap as module
def test_dump_data_sources_creates_manifest_file():
+748
View File
@@ -0,0 +1,748 @@
from flexmock import flexmock
from borgmatic.hooks.data_source import btrfs as module
def test_get_filesystem_mount_points_parses_findmnt_output():
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).and_return(
'/mnt0 /dev/loop0 btrfs rw,relatime,ssd,space_cache=v2,subvolid=5,subvol=/\n'
'/mnt1 /dev/loop1 btrfs rw,relatime,ssd,space_cache=v2,subvolid=5,subvol=/\n'
)
assert module.get_filesystem_mount_points('findmnt') == ('/mnt0', '/mnt1')
def test_get_subvolumes_for_filesystem_parses_subvolume_list_output():
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).and_return(
'ID 270 gen 107 top level 5 path subvol1\nID 272 gen 74 top level 5 path subvol2\n'
)
assert module.get_subvolumes_for_filesystem('btrfs', '/mnt') == (
'/mnt',
'/mnt/subvol1',
'/mnt/subvol2',
)
def test_get_subvolumes_for_filesystem_skips_empty_subvolume_paths():
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).and_return('\n \nID 272 gen 74 top level 5 path subvol2\n')
assert module.get_subvolumes_for_filesystem('btrfs', '/mnt') == ('/mnt', '/mnt/subvol2')
def test_get_subvolumes_for_filesystem_skips_empty_filesystem_mount_points():
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).and_return(
'ID 270 gen 107 top level 5 path subvol1\nID 272 gen 74 top level 5 path subvol2\n'
)
assert module.get_subvolumes_for_filesystem('btrfs', ' ') == ()
def test_get_subvolumes_collects_subvolumes_matching_source_directories_from_all_filesystems():
flexmock(module).should_receive('get_filesystem_mount_points').and_return(('/mnt1', '/mnt2'))
flexmock(module).should_receive('get_subvolumes_for_filesystem').with_args(
'btrfs', '/mnt1'
).and_return(('/one', '/two'))
flexmock(module).should_receive('get_subvolumes_for_filesystem').with_args(
'btrfs', '/mnt2'
).and_return(('/three', '/four'))
for path in ('/one', '/four'):
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_directories'
).with_args(path, object).and_return((path,))
for path in ('/two', '/three'):
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_directories'
).with_args(path, object).and_return(())
assert module.get_subvolumes(
'btrfs', 'findmnt', source_directories=['/one', '/four', '/five', '/six', '/mnt2', '/mnt3']
) == (
module.Subvolume('/four', contained_source_directories=('/four',)),
module.Subvolume('/one', contained_source_directories=('/one',)),
)
def test_get_subvolumes_without_source_directories_collects_all_subvolumes_from_all_filesystems():
flexmock(module).should_receive('get_filesystem_mount_points').and_return(('/mnt1', '/mnt2'))
flexmock(module).should_receive('get_subvolumes_for_filesystem').with_args(
'btrfs', '/mnt1'
).and_return(('/one', '/two'))
flexmock(module).should_receive('get_subvolumes_for_filesystem').with_args(
'btrfs', '/mnt2'
).and_return(('/three', '/four'))
for path in ('/one', '/two', '/three', '/four'):
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_directories'
).with_args(path, object).and_return((path,))
assert module.get_subvolumes('btrfs', 'findmnt') == (
module.Subvolume('/four', contained_source_directories=('/four',)),
module.Subvolume('/one', contained_source_directories=('/one',)),
module.Subvolume('/three', contained_source_directories=('/three',)),
module.Subvolume('/two', contained_source_directories=('/two',)),
)
def test_dump_data_sources_snapshots_each_subvolume_and_updates_source_directories():
source_directories = ['/foo', '/mnt/subvol1']
config = {'btrfs': {}}
flexmock(module).should_receive('get_subvolumes').and_return(
(
module.Subvolume('/mnt/subvol1', contained_source_directories=('/mnt/subvol1',)),
module.Subvolume('/mnt/subvol2', contained_source_directories=('/mnt/subvol2',)),
)
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
'/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
'/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
)
flexmock(module).should_receive('snapshot_subvolume').with_args(
'btrfs', '/mnt/subvol1', '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
).once()
flexmock(module).should_receive('snapshot_subvolume').with_args(
'btrfs', '/mnt/subvol2', '/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
).once()
flexmock(module).should_receive('make_snapshot_exclude_path').with_args(
'/mnt/subvol1'
).and_return('/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234')
flexmock(module).should_receive('make_snapshot_exclude_path').with_args(
'/mnt/subvol2'
).and_return('/mnt/subvol2/.borgmatic-1234/mnt/subvol2/.borgmatic-1234')
flexmock(module).should_receive('make_borg_source_directory_path').with_args(
'/mnt/subvol1', object
).and_return('/mnt/subvol1/.borgmatic-1234/mnt/subvol1')
flexmock(module).should_receive('make_borg_source_directory_path').with_args(
'/mnt/subvol2', object
).and_return('/mnt/subvol2/.borgmatic-1234/mnt/subvol2')
assert (
module.dump_data_sources(
hook_config=config['btrfs'],
config=config,
log_prefix='test',
config_paths=('test.yaml',),
borgmatic_runtime_directory='/run/borgmatic',
source_directories=source_directories,
dry_run=False,
)
== []
)
assert source_directories == [
'/foo',
'/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
'/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
]
assert config == {
'btrfs': {},
'exclude_patterns': [
'/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
'/mnt/subvol2/.borgmatic-1234/mnt/subvol2/.borgmatic-1234',
],
}
def test_dump_data_sources_uses_custom_btrfs_command_in_commands():
source_directories = ['/foo', '/mnt/subvol1']
config = {'btrfs': {'btrfs_command': '/usr/local/bin/btrfs'}}
flexmock(module).should_receive('get_subvolumes').and_return(
(module.Subvolume('/mnt/subvol1', contained_source_directories=('/mnt/subvol1',)),)
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
'/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
)
flexmock(module).should_receive('snapshot_subvolume').with_args(
'/usr/local/bin/btrfs', '/mnt/subvol1', '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
).once()
flexmock(module).should_receive('make_snapshot_exclude_path').with_args(
'/mnt/subvol1'
).and_return('/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234')
flexmock(module).should_receive('make_borg_source_directory_path').with_args(
'/mnt/subvol1', object
).and_return('/mnt/subvol1/.borgmatic-1234/mnt/subvol1')
assert (
module.dump_data_sources(
hook_config=config['btrfs'],
config=config,
log_prefix='test',
config_paths=('test.yaml',),
borgmatic_runtime_directory='/run/borgmatic',
source_directories=source_directories,
dry_run=False,
)
== []
)
assert source_directories == [
'/foo',
'/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
]
assert config == {
'btrfs': {
'btrfs_command': '/usr/local/bin/btrfs',
},
'exclude_patterns': [
'/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
],
}
def test_dump_data_sources_uses_custom_findmnt_command_in_commands():
source_directories = ['/foo', '/mnt/subvol1']
config = {'btrfs': {'findmnt_command': '/usr/local/bin/findmnt'}}
flexmock(module).should_receive('get_subvolumes').with_args(
'btrfs', '/usr/local/bin/findmnt', source_directories
).and_return(
(module.Subvolume('/mnt/subvol1', contained_source_directories=('/mnt/subvol1',)),)
).once()
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
'/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
)
flexmock(module).should_receive('snapshot_subvolume').with_args(
'btrfs', '/mnt/subvol1', '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
).once()
flexmock(module).should_receive('make_snapshot_exclude_path').with_args(
'/mnt/subvol1'
).and_return('/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234')
flexmock(module).should_receive('make_borg_source_directory_path').with_args(
'/mnt/subvol1', object
).and_return('/mnt/subvol1/.borgmatic-1234/mnt/subvol1')
assert (
module.dump_data_sources(
hook_config=config['btrfs'],
config=config,
log_prefix='test',
config_paths=('test.yaml',),
borgmatic_runtime_directory='/run/borgmatic',
source_directories=source_directories,
dry_run=False,
)
== []
)
assert source_directories == [
'/foo',
'/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
]
assert config == {
'btrfs': {
'findmnt_command': '/usr/local/bin/findmnt',
},
'exclude_patterns': [
'/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
],
}
def test_dump_data_sources_with_dry_run_skips_snapshot_and_source_directories_update():
source_directories = ['/foo', '/mnt/subvol1']
config = {'btrfs': {}}
flexmock(module).should_receive('get_subvolumes').and_return(
(module.Subvolume('/mnt/subvol1', contained_source_directories=('/mnt/subvol1',)),)
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
'/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
)
flexmock(module).should_receive('snapshot_subvolume').never()
flexmock(module).should_receive('make_snapshot_exclude_path').never()
assert (
module.dump_data_sources(
hook_config=config['btrfs'],
config=config,
log_prefix='test',
config_paths=('test.yaml',),
borgmatic_runtime_directory='/run/borgmatic',
source_directories=source_directories,
dry_run=True,
)
== []
)
assert source_directories == ['/foo', '/mnt/subvol1']
assert config == {'btrfs': {}}
def test_dump_data_sources_without_matching_subvolumes_skips_snapshot_and_source_directories_update():
source_directories = ['/foo', '/mnt/subvol1']
config = {'btrfs': {}}
flexmock(module).should_receive('get_subvolumes').and_return(())
flexmock(module).should_receive('make_snapshot_path').never()
flexmock(module).should_receive('snapshot_subvolume').never()
flexmock(module).should_receive('make_snapshot_exclude_path').never()
assert (
module.dump_data_sources(
hook_config=config['btrfs'],
config=config,
log_prefix='test',
config_paths=('test.yaml',),
borgmatic_runtime_directory='/run/borgmatic',
source_directories=source_directories,
dry_run=False,
)
== []
)
assert source_directories == ['/foo', '/mnt/subvol1']
assert config == {'btrfs': {}}
def test_dump_data_sources_snapshots_adds_to_existing_exclude_patterns():
source_directories = ['/foo', '/mnt/subvol1']
config = {'btrfs': {}, 'exclude_patterns': ['/bar']}
flexmock(module).should_receive('get_subvolumes').and_return(
(
module.Subvolume('/mnt/subvol1', contained_source_directories=('/mnt/subvol1',)),
module.Subvolume('/mnt/subvol2', contained_source_directories=('/mnt/subvol2',)),
)
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
'/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
'/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
)
flexmock(module).should_receive('snapshot_subvolume').with_args(
'btrfs', '/mnt/subvol1', '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
).once()
flexmock(module).should_receive('snapshot_subvolume').with_args(
'btrfs', '/mnt/subvol2', '/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
).once()
flexmock(module).should_receive('make_snapshot_exclude_path').with_args(
'/mnt/subvol1'
).and_return('/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234')
flexmock(module).should_receive('make_snapshot_exclude_path').with_args(
'/mnt/subvol2'
).and_return('/mnt/subvol2/.borgmatic-1234/mnt/subvol2/.borgmatic-1234')
flexmock(module).should_receive('make_borg_source_directory_path').with_args(
'/mnt/subvol1', object
).and_return('/mnt/subvol1/.borgmatic-1234/mnt/subvol1')
flexmock(module).should_receive('make_borg_source_directory_path').with_args(
'/mnt/subvol2', object
).and_return('/mnt/subvol2/.borgmatic-1234/mnt/subvol2')
assert (
module.dump_data_sources(
hook_config=config['btrfs'],
config=config,
log_prefix='test',
config_paths=('test.yaml',),
borgmatic_runtime_directory='/run/borgmatic',
source_directories=source_directories,
dry_run=False,
)
== []
)
assert source_directories == [
'/foo',
'/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
'/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
]
assert config == {
'btrfs': {},
'exclude_patterns': [
'/bar',
'/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
'/mnt/subvol2/.borgmatic-1234/mnt/subvol2/.borgmatic-1234',
],
}
def test_remove_data_source_dumps_deletes_snapshots():
config = {'btrfs': {}}
flexmock(module).should_receive('get_subvolumes').and_return(
(
module.Subvolume('/mnt/subvol1', contained_source_directories=('/mnt/subvol1',)),
module.Subvolume('/mnt/subvol2', contained_source_directories=('/mnt/subvol2',)),
)
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
'/mnt/subvol1/.borgmatic-1234/./mnt/subvol1'
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
'/mnt/subvol2/.borgmatic-1234/./mnt/subvol2'
)
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).with_args(
'/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
).and_return(
'/mnt/subvol1/.borgmatic-*/mnt/subvol1'
)
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).with_args(
'/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
).and_return(
'/mnt/subvol2/.borgmatic-*/mnt/subvol2'
)
flexmock(module.glob).should_receive('glob').with_args(
'/mnt/subvol1/.borgmatic-*/mnt/subvol1'
).and_return(
('/mnt/subvol1/.borgmatic-1234/mnt/subvol1', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1')
)
flexmock(module.glob).should_receive('glob').with_args(
'/mnt/subvol2/.borgmatic-*/mnt/subvol2'
).and_return(
('/mnt/subvol2/.borgmatic-1234/mnt/subvol2', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2')
)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol1/.borgmatic-5678/mnt/subvol1'
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol2/.borgmatic-5678/mnt/subvol2'
).and_return(False)
flexmock(module).should_receive('delete_snapshot').with_args(
'btrfs', '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
).once()
flexmock(module).should_receive('delete_snapshot').with_args(
'btrfs', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1'
).once()
flexmock(module).should_receive('delete_snapshot').with_args(
'btrfs', '/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
).once()
flexmock(module).should_receive('delete_snapshot').with_args(
'btrfs', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2'
).never()
flexmock(module.shutil).should_receive('rmtree').with_args(
'/mnt/subvol1/.borgmatic-1234'
).once()
flexmock(module.shutil).should_receive('rmtree').with_args(
'/mnt/subvol1/.borgmatic-5678'
).once()
flexmock(module.shutil).should_receive('rmtree').with_args(
'/mnt/subvol2/.borgmatic-1234'
).once()
flexmock(module.shutil).should_receive('rmtree').with_args(
'/mnt/subvol2/.borgmatic-5678'
).never()
module.remove_data_source_dumps(
hook_config=config['btrfs'],
config=config,
log_prefix='test',
borgmatic_runtime_directory='/run/borgmatic',
dry_run=False,
)
def test_remove_data_source_dumps_with_get_subvolumes_file_not_found_error_bails():
config = {'btrfs': {}}
flexmock(module).should_receive('get_subvolumes').and_raise(FileNotFoundError)
flexmock(module).should_receive('make_snapshot_path').never()
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).never()
flexmock(module).should_receive('delete_snapshot').never()
flexmock(module.shutil).should_receive('rmtree').never()
module.remove_data_source_dumps(
hook_config=config['btrfs'],
config=config,
log_prefix='test',
borgmatic_runtime_directory='/run/borgmatic',
dry_run=False,
)
def test_remove_data_source_dumps_with_get_subvolumes_called_process_error_bails():
config = {'btrfs': {}}
flexmock(module).should_receive('get_subvolumes').and_raise(
module.subprocess.CalledProcessError(1, 'command', 'error')
)
flexmock(module).should_receive('make_snapshot_path').never()
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).never()
flexmock(module).should_receive('delete_snapshot').never()
flexmock(module.shutil).should_receive('rmtree').never()
module.remove_data_source_dumps(
hook_config=config['btrfs'],
config=config,
log_prefix='test',
borgmatic_runtime_directory='/run/borgmatic',
dry_run=False,
)
def test_remove_data_source_dumps_with_dry_run_skips_deletes():
config = {'btrfs': {}}
flexmock(module).should_receive('get_subvolumes').and_return(
(
module.Subvolume('/mnt/subvol1', contained_source_directories=('/mnt/subvol1',)),
module.Subvolume('/mnt/subvol2', contained_source_directories=('/mnt/subvol2',)),
)
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
'/mnt/subvol1/.borgmatic-1234/./mnt/subvol1'
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
'/mnt/subvol2/.borgmatic-1234/./mnt/subvol2'
)
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).with_args(
'/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
).and_return(
'/mnt/subvol1/.borgmatic-*/mnt/subvol1'
)
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).with_args(
'/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
).and_return(
'/mnt/subvol2/.borgmatic-*/mnt/subvol2'
)
flexmock(module.glob).should_receive('glob').with_args(
'/mnt/subvol1/.borgmatic-*/mnt/subvol1'
).and_return(
('/mnt/subvol1/.borgmatic-1234/mnt/subvol1', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1')
)
flexmock(module.glob).should_receive('glob').with_args(
'/mnt/subvol2/.borgmatic-*/mnt/subvol2'
).and_return(
('/mnt/subvol2/.borgmatic-1234/mnt/subvol2', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2')
)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol1/.borgmatic-5678/mnt/subvol1'
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol2/.borgmatic-5678/mnt/subvol2'
).and_return(False)
flexmock(module).should_receive('delete_snapshot').never()
flexmock(module.shutil).should_receive('rmtree').never()
module.remove_data_source_dumps(
hook_config=config['btrfs'],
config=config,
log_prefix='test',
borgmatic_runtime_directory='/run/borgmatic',
dry_run=True,
)
def test_remove_data_source_dumps_without_subvolumes_skips_deletes():
config = {'btrfs': {}}
flexmock(module).should_receive('get_subvolumes').and_return(())
flexmock(module).should_receive('make_snapshot_path').never()
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).never()
flexmock(module).should_receive('delete_snapshot').never()
flexmock(module.shutil).should_receive('rmtree').never()
module.remove_data_source_dumps(
hook_config=config['btrfs'],
config=config,
log_prefix='test',
borgmatic_runtime_directory='/run/borgmatic',
dry_run=False,
)
def test_remove_data_source_without_snapshots_skips_deletes():
config = {'btrfs': {}}
flexmock(module).should_receive('get_subvolumes').and_return(
(
module.Subvolume('/mnt/subvol1', contained_source_directories=('/mnt/subvol1',)),
module.Subvolume('/mnt/subvol2', contained_source_directories=('/mnt/subvol2',)),
)
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
'/mnt/subvol1/.borgmatic-1234/./mnt/subvol1'
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
'/mnt/subvol2/.borgmatic-1234/./mnt/subvol2'
)
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).with_args(
'/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
).and_return(
'/mnt/subvol1/.borgmatic-*/mnt/subvol1'
)
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).with_args(
'/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
).and_return(
'/mnt/subvol2/.borgmatic-*/mnt/subvol2'
)
flexmock(module.glob).should_receive('glob').and_return(())
flexmock(module.os.path).should_receive('isdir').never()
flexmock(module).should_receive('delete_snapshot').never()
flexmock(module.shutil).should_receive('rmtree').never()
module.remove_data_source_dumps(
hook_config=config['btrfs'],
config=config,
log_prefix='test',
borgmatic_runtime_directory='/run/borgmatic',
dry_run=False,
)
def test_remove_data_source_dumps_with_delete_snapshot_file_not_found_error_bails():
config = {'btrfs': {}}
flexmock(module).should_receive('get_subvolumes').and_return(
(
module.Subvolume('/mnt/subvol1', contained_source_directories=('/mnt/subvol1',)),
module.Subvolume('/mnt/subvol2', contained_source_directories=('/mnt/subvol2',)),
)
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
'/mnt/subvol1/.borgmatic-1234/./mnt/subvol1'
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
'/mnt/subvol2/.borgmatic-1234/./mnt/subvol2'
)
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).with_args(
'/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
).and_return(
'/mnt/subvol1/.borgmatic-*/mnt/subvol1'
)
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).with_args(
'/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
).and_return(
'/mnt/subvol2/.borgmatic-*/mnt/subvol2'
)
flexmock(module.glob).should_receive('glob').with_args(
'/mnt/subvol1/.borgmatic-*/mnt/subvol1'
).and_return(
('/mnt/subvol1/.borgmatic-1234/mnt/subvol1', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1')
)
flexmock(module.glob).should_receive('glob').with_args(
'/mnt/subvol2/.borgmatic-*/mnt/subvol2'
).and_return(
('/mnt/subvol2/.borgmatic-1234/mnt/subvol2', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2')
)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol1/.borgmatic-5678/mnt/subvol1'
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol2/.borgmatic-5678/mnt/subvol2'
).and_return(False)
flexmock(module).should_receive('delete_snapshot').and_raise(FileNotFoundError)
flexmock(module.shutil).should_receive('rmtree').never()
module.remove_data_source_dumps(
hook_config=config['btrfs'],
config=config,
log_prefix='test',
borgmatic_runtime_directory='/run/borgmatic',
dry_run=False,
)
def test_remove_data_source_dumps_with_delete_snapshot_called_process_error_bails():
config = {'btrfs': {}}
flexmock(module).should_receive('get_subvolumes').and_return(
(
module.Subvolume('/mnt/subvol1', contained_source_directories=('/mnt/subvol1',)),
module.Subvolume('/mnt/subvol2', contained_source_directories=('/mnt/subvol2',)),
)
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
'/mnt/subvol1/.borgmatic-1234/./mnt/subvol1'
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
'/mnt/subvol2/.borgmatic-1234/./mnt/subvol2'
)
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).with_args(
'/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
).and_return(
'/mnt/subvol1/.borgmatic-*/mnt/subvol1'
)
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).with_args(
'/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
).and_return(
'/mnt/subvol2/.borgmatic-*/mnt/subvol2'
)
flexmock(module.glob).should_receive('glob').with_args(
'/mnt/subvol1/.borgmatic-*/mnt/subvol1'
).and_return(
('/mnt/subvol1/.borgmatic-1234/mnt/subvol1', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1')
)
flexmock(module.glob).should_receive('glob').with_args(
'/mnt/subvol2/.borgmatic-*/mnt/subvol2'
).and_return(
('/mnt/subvol2/.borgmatic-1234/mnt/subvol2', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2')
)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol1/.borgmatic-5678/mnt/subvol1'
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol2/.borgmatic-5678/mnt/subvol2'
).and_return(False)
flexmock(module).should_receive('delete_snapshot').and_raise(
module.subprocess.CalledProcessError(1, 'command', 'error')
)
flexmock(module.shutil).should_receive('rmtree').never()
module.remove_data_source_dumps(
hook_config=config['btrfs'],
config=config,
log_prefix='test',
borgmatic_runtime_directory='/run/borgmatic',
dry_run=False,
)
@@ -1,7 +1,7 @@
import pytest
from flexmock import flexmock
from borgmatic.hooks import dump as module
from borgmatic.hooks.data_source import dump as module
def test_make_data_source_dump_path_joins_arguments():
File diff suppressed because it is too large Load Diff
@@ -3,7 +3,7 @@ import logging
import pytest
from flexmock import flexmock
from borgmatic.hooks import mariadb as module
from borgmatic.hooks.data_source import mariadb as module
def test_database_names_to_dump_passes_through_name():
@@ -2,7 +2,7 @@ import logging
from flexmock import flexmock
from borgmatic.hooks import mongodb as module
from borgmatic.hooks.data_source import mongodb as module
def test_use_streaming_true_for_any_non_directory_format_databases():
@@ -3,7 +3,7 @@ import logging
import pytest
from flexmock import flexmock
from borgmatic.hooks import mysql as module
from borgmatic.hooks.data_source import mysql as module
def test_database_names_to_dump_passes_through_name():
@@ -3,7 +3,7 @@ import logging
import pytest
from flexmock import flexmock
from borgmatic.hooks import postgresql as module
from borgmatic.hooks.data_source import postgresql as module
def test_make_extra_environment_maps_options_to_environment():
@@ -0,0 +1,26 @@
from borgmatic.hooks.data_source import snapshot as module
def test_get_contained_directories_without_candidates_returns_empty():
assert module.get_contained_directories('/mnt', {}) == ()
def test_get_contained_directories_with_self_candidate_returns_self():
candidates = {'/foo', '/mnt', '/bar'}
assert module.get_contained_directories('/mnt', candidates) == ('/mnt',)
assert candidates == {'/foo', '/bar'}
def test_get_contained_directories_with_child_candidate_returns_child():
candidates = {'/foo', '/mnt/subdir', '/bar'}
assert module.get_contained_directories('/mnt', candidates) == ('/mnt/subdir',)
assert candidates == {'/foo', '/bar'}
def test_get_contained_directories_with_grandchild_candidate_returns_child():
candidates = {'/foo', '/mnt/sub/dir', '/bar'}
assert module.get_contained_directories('/mnt', candidates) == ('/mnt/sub/dir',)
assert candidates == {'/foo', '/bar'}
@@ -2,7 +2,7 @@ import logging
from flexmock import flexmock
from borgmatic.hooks import sqlite as module
from borgmatic.hooks.data_source import sqlite as module
def test_use_streaming_true_for_any_databases():
@@ -1,62 +1,95 @@
import os
import pytest
from flexmock import flexmock
import borgmatic.execute
from borgmatic.hooks import zfs as module
from borgmatic.hooks.data_source 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(
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).and_return(
'dataset\t/dataset\t-\nother\t/other\t-',
)
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_directories'
).with_args('/dataset', object).and_return(('/dataset',))
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_directories'
).with_args('/other', object).and_return(())
assert module.get_datasets_to_backup(
'zfs', source_directories=('/foo', '/dataset', '/bar')
) == (('dataset', '/dataset'),)
) == (
module.Dataset(
name='dataset', mount_point='/dataset', contained_source_directories=('/dataset',)
),
)
def test_get_datasets_to_backup_filters_datasets_by_user_property():
flexmock(borgmatic.execute).should_receive('execute_command_and_capture_output').and_return(
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).and_return(
'dataset\t/dataset\tauto\nother\t/other\t-',
)
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_directories'
).with_args('/dataset', object).never()
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_directories'
).with_args('/other', object).and_return(())
assert module.get_datasets_to_backup('zfs', source_directories=('/foo', '/bar')) == (
('dataset', '/dataset'),
module.Dataset(
name='dataset',
mount_point='/dataset',
auto_backup=True,
contained_source_directories=('/dataset',),
),
)
def test_get_datasets_to_backup_with_invalid_list_output_raises():
flexmock(borgmatic.execute).should_receive('execute_command_and_capture_output').and_return(
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).and_return(
'dataset',
)
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_directories'
).never()
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',
def test_get_all_dataset_mount_points_does_not_filter_datasets():
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).and_return(
'/dataset\n/other',
)
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
'get_contained_directories'
).and_return(('/dataset',))
assert module.get_all_datasets('zfs') == (
('dataset', '/dataset'),
('other', '/other'),
assert module.get_all_dataset_mount_points('zfs') == (
('/dataset'),
('/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(
name='dataset',
mount_point='/mnt/dataset',
contained_source_directories=('/mnt/dataset/subdir',),
)
)
)
flexmock(module.os).should_receive('getpid').and_return(1234)
full_snapshot_name = 'dataset@borgmatic-1234'
@@ -70,6 +103,29 @@ def test_dump_data_sources_snapshots_and_mounts_and_updates_source_directories()
full_snapshot_name,
module.os.path.normpath(snapshot_mount_path),
).once()
source_directories = ['/mnt/dataset/subdir']
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 == [os.path.join(snapshot_mount_path, 'subdir')]
def test_dump_data_sources_with_no_datasets_skips_snapshots():
flexmock(module).should_receive('get_datasets_to_backup').and_return(())
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 (
@@ -85,12 +141,18 @@ def test_dump_data_sources_snapshots_and_mounts_and_updates_source_directories()
== []
)
assert source_directories == [snapshot_mount_path]
assert source_directories == ['/mnt/dataset']
def test_dump_data_sources_uses_custom_commands():
flexmock(module).should_receive('get_datasets_to_backup').and_return(
(('dataset', '/mnt/dataset'),)
(
flexmock(
name='dataset',
mount_point='/mnt/dataset',
contained_source_directories=('/mnt/dataset/subdir',),
)
)
)
flexmock(module.os).should_receive('getpid').and_return(1234)
full_snapshot_name = 'dataset@borgmatic-1234'
@@ -104,7 +166,7 @@ def test_dump_data_sources_uses_custom_commands():
full_snapshot_name,
module.os.path.normpath(snapshot_mount_path),
).once()
source_directories = ['/mnt/dataset']
source_directories = ['/mnt/dataset/subdir']
hook_config = {
'zfs_command': '/usr/local/bin/zfs',
'mount_command': '/usr/local/bin/mount',
@@ -126,12 +188,12 @@ def test_dump_data_sources_uses_custom_commands():
== []
)
assert source_directories == [snapshot_mount_path]
assert source_directories == [os.path.join(snapshot_mount_path, 'subdir')]
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(name='dataset', mount_point='/mnt/dataset'),)
)
flexmock(module.os).should_receive('getpid').and_return(1234)
flexmock(module).should_receive('snapshot_dataset').never()
@@ -154,8 +216,50 @@ def test_dump_data_sources_with_dry_run_skips_commands_and_does_not_touch_source
assert source_directories == ['/mnt/dataset']
def test_dump_data_sources_ignores_mismatch_between_source_directories_and_contained_source_directories():
flexmock(module).should_receive('get_datasets_to_backup').and_return(
(
flexmock(
name='dataset',
mount_point='/mnt/dataset',
contained_source_directories=('/mnt/dataset/subdir',),
)
)
)
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 = ['/hmm']
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 == ['/hmm', os.path.join(snapshot_mount_path, 'subdir')]
def test_get_all_snapshots_parses_list_output():
flexmock(borgmatic.execute).should_receive('execute_command_and_capture_output').and_return(
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).and_return(
'dataset1@borgmatic-1234\ndataset2@borgmatic-4567',
)
@@ -163,7 +267,7 @@ def test_get_all_snapshots_parses_list_output():
def test_remove_data_source_dumps_unmounts_and_destroys_snapshots():
flexmock(module).should_receive('get_all_datasets').and_return((('dataset', '/mnt/dataset'),))
flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).and_return('/run/borgmatic')
@@ -190,7 +294,7 @@ def test_remove_data_source_dumps_unmounts_and_destroys_snapshots():
def test_remove_data_source_dumps_use_custom_commands():
flexmock(module).should_receive('get_all_datasets').and_return((('dataset', '/mnt/dataset'),))
flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).and_return('/run/borgmatic')
@@ -218,7 +322,7 @@ def test_remove_data_source_dumps_use_custom_commands():
def test_remove_data_source_dumps_bails_for_missing_zfs_command():
flexmock(module).should_receive('get_all_datasets').and_raise(FileNotFoundError)
flexmock(module).should_receive('get_all_dataset_mount_points').and_raise(FileNotFoundError)
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).never()
@@ -234,7 +338,7 @@ def test_remove_data_source_dumps_bails_for_missing_zfs_command():
def test_remove_data_source_dumps_bails_for_zfs_command_error():
flexmock(module).should_receive('get_all_datasets').and_raise(
flexmock(module).should_receive('get_all_dataset_mount_points').and_raise(
module.subprocess.CalledProcessError(1, 'wtf')
)
flexmock(module.borgmatic.config.paths).should_receive(
@@ -252,7 +356,7 @@ def test_remove_data_source_dumps_bails_for_zfs_command_error():
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).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).and_return('/run/borgmatic')
@@ -276,7 +380,7 @@ def test_remove_data_source_dumps_bails_for_missing_umount_command():
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).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).and_return('/run/borgmatic')
@@ -300,7 +404,7 @@ def test_remove_data_source_dumps_bails_for_umount_command_error():
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).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).and_return('/run/borgmatic')
@@ -325,7 +429,7 @@ def test_remove_data_source_dumps_skips_unmount_snapshot_directories_that_are_no
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).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).and_return('/run/borgmatic')
@@ -354,8 +458,38 @@ def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_that_are_no
)
def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_after_rmtree_succeeds():
flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/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(True).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).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob'
).and_return('/run/borgmatic')
@@ -2,8 +2,8 @@ import apprise
from apprise import NotifyFormat, NotifyType
from flexmock import flexmock
import borgmatic.hooks.monitor
from borgmatic.hooks import apprise as module
import borgmatic.hooks.monitoring.monitor
from borgmatic.hooks.monitoring import apprise as module
TOPIC = 'borgmatic-unit-testing'
@@ -18,7 +18,7 @@ def mock_apprise():
def test_initialize_monitor_with_send_logs_false_does_not_add_handler():
flexmock(module.borgmatic.hooks.logs).should_receive('add_handler').never()
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('add_handler').never()
module.initialize_monitor(
hook_config={'send_logs': False},
@@ -31,12 +31,14 @@ def test_initialize_monitor_with_send_logs_false_does_not_add_handler():
def test_initialize_monitor_with_send_logs_true_adds_handler_with_default_log_size_limit():
truncation_indicator_length = 4
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').with_args(
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'Forgetful_buffering_handler'
).with_args(
module.HANDLER_IDENTIFIER,
module.DEFAULT_LOGS_SIZE_LIMIT_BYTES - truncation_indicator_length,
1,
).once()
flexmock(module.borgmatic.hooks.logs).should_receive('add_handler').once()
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('add_handler').once()
module.initialize_monitor(
hook_config={'send_logs': True},
@@ -49,12 +51,14 @@ def test_initialize_monitor_with_send_logs_true_adds_handler_with_default_log_si
def test_initialize_monitor_without_send_logs_adds_handler_with_default_log_size_limit():
truncation_indicator_length = 4
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').with_args(
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'Forgetful_buffering_handler'
).with_args(
module.HANDLER_IDENTIFIER,
module.DEFAULT_LOGS_SIZE_LIMIT_BYTES - truncation_indicator_length,
1,
).once()
flexmock(module.borgmatic.hooks.logs).should_receive('add_handler').once()
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('add_handler').once()
module.initialize_monitor(
hook_config={},
@@ -66,8 +70,8 @@ def test_initialize_monitor_without_send_logs_adds_handler_with_default_log_size
def test_ping_monitor_respects_dry_run():
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.logs).should_receive(
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload'
).and_return('loggy log')
mock_apprise().should_receive('notify').never()
@@ -76,30 +80,32 @@ def test_ping_monitor_respects_dry_run():
{'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}]},
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=True,
)
def test_ping_monitor_with_no_states_does_not_notify():
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler').never()
flexmock(module.borgmatic.hooks.logs).should_receive('format_buffered_logs_for_payload').never()
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler').never()
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload'
).never()
mock_apprise().should_receive('notify').never()
module.ping_monitor(
{'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}], 'states': []},
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=True,
)
def test_ping_monitor_notifies_fail_by_default():
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.logs).should_receive(
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload'
).and_return('')
mock_apprise().should_receive('notify').with_args(
@@ -109,7 +115,7 @@ def test_ping_monitor_notifies_fail_by_default():
notify_type=NotifyType.FAILURE,
).once()
for state in borgmatic.hooks.monitor.State:
for state in borgmatic.hooks.monitoring.monitor.State:
module.ping_monitor(
{'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}]},
{},
@@ -121,8 +127,8 @@ def test_ping_monitor_notifies_fail_by_default():
def test_ping_monitor_with_logs_appends_logs_to_body():
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.logs).should_receive(
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload'
).and_return('loggy log')
mock_apprise().should_receive('notify').with_args(
@@ -132,7 +138,7 @@ def test_ping_monitor_with_logs_appends_logs_to_body():
notify_type=NotifyType.FAILURE,
).once()
for state in borgmatic.hooks.monitor.State:
for state in borgmatic.hooks.monitoring.monitor.State:
module.ping_monitor(
{'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}]},
{},
@@ -144,8 +150,8 @@ def test_ping_monitor_with_logs_appends_logs_to_body():
def test_ping_monitor_with_finish_default_config_notifies():
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.logs).should_receive(
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload'
).and_return('')
mock_apprise().should_receive('notify').with_args(
@@ -159,15 +165,17 @@ def test_ping_monitor_with_finish_default_config_notifies():
{'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}], 'states': ['finish']},
{},
'config.yaml',
borgmatic.hooks.monitor.State.FINISH,
borgmatic.hooks.monitoring.monitor.State.FINISH,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_with_start_default_config_notifies():
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler').never()
flexmock(module.borgmatic.hooks.logs).should_receive('format_buffered_logs_for_payload').never()
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler').never()
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload'
).never()
mock_apprise().should_receive('notify').with_args(
title='A borgmatic START event happened',
body='A borgmatic START event happened',
@@ -179,15 +187,15 @@ def test_ping_monitor_with_start_default_config_notifies():
{'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}], 'states': ['start']},
{},
'config.yaml',
borgmatic.hooks.monitor.State.START,
borgmatic.hooks.monitoring.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_with_fail_default_config_notifies():
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.logs).should_receive(
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload'
).and_return('')
mock_apprise().should_receive('notify').with_args(
@@ -201,15 +209,15 @@ def test_ping_monitor_with_fail_default_config_notifies():
{'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}], 'states': ['fail']},
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_with_log_default_config_notifies():
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.logs).should_receive(
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload'
).and_return('')
mock_apprise().should_receive('notify').with_args(
@@ -223,15 +231,15 @@ def test_ping_monitor_with_log_default_config_notifies():
{'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}], 'states': ['log']},
{},
'config.yaml',
borgmatic.hooks.monitor.State.LOG,
borgmatic.hooks.monitoring.monitor.State.LOG,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_passes_through_custom_message_title():
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.logs).should_receive(
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload'
).and_return('')
mock_apprise().should_receive('notify').with_args(
@@ -249,15 +257,15 @@ def test_ping_monitor_passes_through_custom_message_title():
},
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_passes_through_custom_message_body():
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.logs).should_receive(
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload'
).and_return('')
mock_apprise().should_receive('notify').with_args(
@@ -275,15 +283,15 @@ def test_ping_monitor_passes_through_custom_message_body():
},
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_passes_through_custom_message_body_and_appends_logs():
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.logs).should_receive(
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload'
).and_return('loggy log')
mock_apprise().should_receive('notify').with_args(
@@ -301,15 +309,15 @@ def test_ping_monitor_passes_through_custom_message_body_and_appends_logs():
},
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_pings_multiple_services():
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.logs).should_receive(
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload'
).and_return('')
mock_apprise().should_receive('add').with_args([f'ntfys://{TOPIC}', f'ntfy://{TOPIC}']).once()
@@ -323,36 +331,38 @@ def test_ping_monitor_pings_multiple_services():
},
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_logs_info_for_no_services():
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler').never()
flexmock(module.borgmatic.hooks.logs).should_receive('format_buffered_logs_for_payload').never()
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler').never()
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload'
).never()
flexmock(module.logger).should_receive('info').once()
module.ping_monitor(
{'services': []},
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_logs_warning_when_notify_fails():
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.logs).should_receive(
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload'
).and_return('')
mock_apprise().should_receive('notify').and_return(False)
flexmock(module.logger).should_receive('warning').once()
for state in borgmatic.hooks.monitor.State:
for state in borgmatic.hooks.monitoring.monitor.State:
module.ping_monitor(
{'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}]},
{},
@@ -364,7 +374,7 @@ def test_ping_monitor_logs_warning_when_notify_fails():
def test_destroy_monitor_does_not_raise():
flexmock(module.borgmatic.hooks.logs).should_receive('remove_handler')
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('remove_handler')
module.destroy_monitor(
hook_config={},
@@ -1,6 +1,6 @@
from flexmock import flexmock
from borgmatic.hooks import cronhub as module
from borgmatic.hooks.monitoring import cronhub as module
def test_ping_monitor_rewrites_ping_url_for_start_state():
@@ -1,6 +1,6 @@
from flexmock import flexmock
from borgmatic.hooks import cronitor as module
from borgmatic.hooks.monitoring import cronitor as module
def test_ping_monitor_hits_ping_url_for_start_state():
@@ -1,6 +1,6 @@
from flexmock import flexmock
from borgmatic.hooks import healthchecks as module
from borgmatic.hooks.monitoring import healthchecks as module
def mock_logger():
@@ -15,9 +15,11 @@ def test_initialize_monitor_creates_log_handler_with_ping_body_limit():
monitoring_log_level = 1
mock_logger()
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').with_args(
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'Forgetful_buffering_handler'
).with_args(
module.HANDLER_IDENTIFIER,
ping_body_limit - len(module.borgmatic.hooks.logs.PAYLOAD_TRUNCATION_INDICATOR),
ping_body_limit - len(module.borgmatic.hooks.monitoring.logs.PAYLOAD_TRUNCATION_INDICATOR),
monitoring_log_level,
).once()
@@ -30,10 +32,12 @@ def test_initialize_monitor_creates_log_handler_with_default_ping_body_limit():
monitoring_log_level = 1
mock_logger()
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').with_args(
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'Forgetful_buffering_handler'
).with_args(
module.HANDLER_IDENTIFIER,
module.DEFAULT_PING_BODY_LIMIT_BYTES
- len(module.borgmatic.hooks.logs.PAYLOAD_TRUNCATION_INDICATOR),
- len(module.borgmatic.hooks.monitoring.logs.PAYLOAD_TRUNCATION_INDICATOR),
monitoring_log_level,
).once()
@@ -45,9 +49,9 @@ def test_initialize_monitor_creates_log_handler_with_zero_ping_body_limit():
monitoring_log_level = 1
mock_logger()
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').with_args(
module.HANDLER_IDENTIFIER, ping_body_limit, monitoring_log_level
).once()
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'Forgetful_buffering_handler'
).with_args(module.HANDLER_IDENTIFIER, ping_body_limit, monitoring_log_level).once()
module.initialize_monitor(
{'ping_body_limit': ping_body_limit}, {}, 'test.yaml', monitoring_log_level, dry_run=False
@@ -56,7 +60,9 @@ def test_initialize_monitor_creates_log_handler_with_zero_ping_body_limit():
def test_initialize_monitor_creates_log_handler_when_send_logs_true():
mock_logger()
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').once()
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'Forgetful_buffering_handler'
).once()
module.initialize_monitor(
{'send_logs': True}, {}, 'test.yaml', monitoring_log_level=1, dry_run=False
@@ -65,7 +71,9 @@ def test_initialize_monitor_creates_log_handler_when_send_logs_true():
def test_initialize_monitor_bails_when_send_logs_false():
mock_logger()
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'Forgetful_buffering_handler'
).never()
module.initialize_monitor(
{'send_logs': False}, {}, 'test.yaml', monitoring_log_level=1, dry_run=False
@@ -73,7 +81,9 @@ def test_initialize_monitor_bails_when_send_logs_false():
def test_ping_monitor_hits_ping_url_for_start_state():
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'Forgetful_buffering_handler'
).never()
hook_config = {'ping_url': 'https://example.com'}
flexmock(module.requests).should_receive('post').with_args(
'https://example.com/start', data=''.encode('utf-8'), verify=True
@@ -92,8 +102,8 @@ def test_ping_monitor_hits_ping_url_for_start_state():
def test_ping_monitor_hits_ping_url_for_finish_state():
hook_config = {'ping_url': 'https://example.com'}
payload = 'data'
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.logs).should_receive(
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload'
).and_return(payload)
flexmock(module.requests).should_receive('post').with_args(
@@ -113,8 +123,8 @@ def test_ping_monitor_hits_ping_url_for_finish_state():
def test_ping_monitor_hits_ping_url_for_fail_state():
hook_config = {'ping_url': 'https://example.com'}
payload = 'data'
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.logs).should_receive(
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload'
).and_return(payload)
flexmock(module.requests).should_receive('post').with_args(
@@ -134,8 +144,8 @@ def test_ping_monitor_hits_ping_url_for_fail_state():
def test_ping_monitor_hits_ping_url_for_log_state():
hook_config = {'ping_url': 'https://example.com'}
payload = 'data'
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.logs).should_receive(
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload'
).and_return(payload)
flexmock(module.requests).should_receive('post').with_args(
@@ -155,8 +165,8 @@ def test_ping_monitor_hits_ping_url_for_log_state():
def test_ping_monitor_with_ping_uuid_hits_corresponding_url():
hook_config = {'ping_url': 'abcd-efgh-ijkl-mnop'}
payload = 'data'
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.logs).should_receive(
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload'
).and_return(payload)
flexmock(module.requests).should_receive('post').with_args(
@@ -178,8 +188,8 @@ def test_ping_monitor_with_ping_uuid_hits_corresponding_url():
def test_ping_monitor_skips_ssl_verification_when_verify_tls_false():
hook_config = {'ping_url': 'https://example.com', 'verify_tls': False}
payload = 'data'
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.logs).should_receive(
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload'
).and_return(payload)
flexmock(module.requests).should_receive('post').with_args(
@@ -199,8 +209,8 @@ def test_ping_monitor_skips_ssl_verification_when_verify_tls_false():
def test_ping_monitor_executes_ssl_verification_when_verify_tls_true():
hook_config = {'ping_url': 'https://example.com', 'verify_tls': True}
payload = 'data'
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.logs).should_receive(
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'format_buffered_logs_for_payload'
).and_return(payload)
flexmock(module.requests).should_receive('post').with_args(
@@ -218,7 +228,9 @@ def test_ping_monitor_executes_ssl_verification_when_verify_tls_true():
def test_ping_monitor_dry_run_does_not_hit_ping_url():
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'Forgetful_buffering_handler'
).never()
hook_config = {'ping_url': 'https://example.com'}
flexmock(module.requests).should_receive('post').never()
@@ -233,7 +245,9 @@ def test_ping_monitor_dry_run_does_not_hit_ping_url():
def test_ping_monitor_does_not_hit_ping_url_when_states_not_matching():
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'Forgetful_buffering_handler'
).never()
hook_config = {'ping_url': 'https://example.com', 'states': ['finish']}
flexmock(module.requests).should_receive('post').never()
@@ -248,7 +262,9 @@ def test_ping_monitor_does_not_hit_ping_url_when_states_not_matching():
def test_ping_monitor_hits_ping_url_when_states_matching():
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'Forgetful_buffering_handler'
).never()
hook_config = {'ping_url': 'https://example.com', 'states': ['start', 'finish']}
flexmock(module.requests).should_receive('post').with_args(
'https://example.com/start', data=''.encode('utf-8'), verify=True
@@ -265,7 +281,9 @@ def test_ping_monitor_hits_ping_url_when_states_matching():
def test_ping_monitor_adds_create_query_parameter_when_create_slug_true():
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'Forgetful_buffering_handler'
).never()
hook_config = {'ping_url': 'https://example.com', 'create_slug': True}
flexmock(module.requests).should_receive('post').with_args(
'https://example.com/start?create=1', data=''.encode('utf-8'), verify=True
@@ -282,7 +300,9 @@ def test_ping_monitor_adds_create_query_parameter_when_create_slug_true():
def test_ping_monitor_does_not_add_create_query_parameter_when_create_slug_false():
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'Forgetful_buffering_handler'
).never()
hook_config = {'ping_url': 'https://example.com', 'create_slug': False}
flexmock(module.requests).should_receive('post').with_args(
'https://example.com/start', data=''.encode('utf-8'), verify=True
@@ -334,7 +354,9 @@ def test_ping_monitor_issues_warning_when_ping_url_is_uuid_and_create_slug_true(
def test_ping_monitor_with_connection_error_logs_warning():
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'Forgetful_buffering_handler'
).never()
hook_config = {'ping_url': 'https://example.com'}
flexmock(module.requests).should_receive('post').with_args(
'https://example.com/start', data=''.encode('utf-8'), verify=True
@@ -352,7 +374,9 @@ def test_ping_monitor_with_connection_error_logs_warning():
def test_ping_monitor_with_other_error_logs_warning():
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
'Forgetful_buffering_handler'
).never()
hook_config = {'ping_url': 'https://example.com'}
response = flexmock(ok=False)
response.should_receive('raise_for_status').and_raise(
@@ -1,7 +1,7 @@
import pytest
from flexmock import flexmock
from borgmatic.hooks import logs as module
from borgmatic.hooks.monitoring import logs as module
def test_forgetful_buffering_handler_emit_collects_log_records():
@@ -3,7 +3,7 @@ import json
import requests
from flexmock import flexmock
from borgmatic.hooks import loki as module
from borgmatic.hooks.monitoring import loki as module
def test_loki_log_buffer_add_value_gets_raw():
@@ -2,8 +2,8 @@ from enum import Enum
from flexmock import flexmock
import borgmatic.hooks.monitor
from borgmatic.hooks import ntfy as module
import borgmatic.hooks.monitoring.monitor
from borgmatic.hooks.monitoring import ntfy as module
default_base_url = 'https://ntfy.sh'
custom_base_url = 'https://ntfy.example.com'
@@ -38,7 +38,7 @@ def test_ping_monitor_minimal_config_hits_hosted_ntfy_on_fail():
hook_config = {'topic': topic}
flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}',
headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL),
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
auth=None,
).and_return(flexmock(ok=True)).once()
@@ -46,7 +46,7 @@ def test_ping_monitor_minimal_config_hits_hosted_ntfy_on_fail():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -59,7 +59,7 @@ def test_ping_monitor_with_access_token_hits_hosted_ntfy_on_fail():
}
flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}',
headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL),
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
auth=module.requests.auth.HTTPBasicAuth('', 'abc123'),
).and_return(flexmock(ok=True)).once()
@@ -67,7 +67,7 @@ def test_ping_monitor_with_access_token_hits_hosted_ntfy_on_fail():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -82,7 +82,7 @@ def test_ping_monitor_with_username_password_and_access_token_ignores_username_p
}
flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}',
headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL),
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
auth=module.requests.auth.HTTPBasicAuth('', 'abc123'),
).and_return(flexmock(ok=True)).once()
flexmock(module.logger).should_receive('warning').once()
@@ -91,7 +91,7 @@ def test_ping_monitor_with_username_password_and_access_token_ignores_username_p
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -105,7 +105,7 @@ def test_ping_monitor_with_username_password_hits_hosted_ntfy_on_fail():
}
flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}',
headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL),
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
auth=module.requests.auth.HTTPBasicAuth('testuser', 'fakepassword'),
).and_return(flexmock(ok=True)).once()
@@ -113,7 +113,7 @@ def test_ping_monitor_with_username_password_hits_hosted_ntfy_on_fail():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -123,7 +123,7 @@ def test_ping_monitor_with_password_but_no_username_warns():
hook_config = {'topic': topic, 'password': 'fakepassword'}
flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}',
headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL),
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
auth=None,
).and_return(flexmock(ok=True)).once()
flexmock(module.logger).should_receive('warning').once()
@@ -132,7 +132,7 @@ def test_ping_monitor_with_password_but_no_username_warns():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -142,7 +142,7 @@ def test_ping_monitor_with_username_but_no_password_warns():
hook_config = {'topic': topic, 'username': 'testuser'}
flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}',
headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL),
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
auth=None,
).and_return(flexmock(ok=True)).once()
flexmock(module.logger).should_receive('warning').once()
@@ -151,7 +151,7 @@ def test_ping_monitor_with_username_but_no_password_warns():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -165,7 +165,7 @@ def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_start():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.START,
borgmatic.hooks.monitoring.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
)
@@ -179,7 +179,7 @@ def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_finish():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FINISH,
borgmatic.hooks.monitoring.monitor.State.FINISH,
monitoring_log_level=1,
dry_run=False,
)
@@ -189,7 +189,7 @@ def test_ping_monitor_minimal_config_hits_selfhosted_ntfy_on_fail():
hook_config = {'topic': topic, 'server': custom_base_url}
flexmock(module.requests).should_receive('post').with_args(
f'{custom_base_url}/{topic}',
headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL),
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
auth=None,
).and_return(flexmock(ok=True)).once()
@@ -197,7 +197,7 @@ def test_ping_monitor_minimal_config_hits_selfhosted_ntfy_on_fail():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -211,7 +211,7 @@ def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_fail_dry_run():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=True,
)
@@ -227,7 +227,7 @@ def test_ping_monitor_custom_message_hits_hosted_ntfy_on_fail():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -237,7 +237,7 @@ def test_ping_monitor_custom_state_hits_hosted_ntfy_on_start():
hook_config = {'topic': topic, 'states': ['start', 'fail']}
flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}',
headers=return_default_message_headers(borgmatic.hooks.monitor.State.START),
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.START),
auth=None,
).and_return(flexmock(ok=True)).once()
@@ -245,7 +245,7 @@ def test_ping_monitor_custom_state_hits_hosted_ntfy_on_start():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.START,
borgmatic.hooks.monitoring.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
)
@@ -255,7 +255,7 @@ def test_ping_monitor_with_connection_error_logs_warning():
hook_config = {'topic': topic}
flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}',
headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL),
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
auth=None,
).and_raise(module.requests.exceptions.ConnectionError)
flexmock(module.logger).should_receive('warning').once()
@@ -264,7 +264,7 @@ def test_ping_monitor_with_connection_error_logs_warning():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -278,7 +278,7 @@ def test_ping_monitor_with_other_error_logs_warning():
)
flexmock(module.requests).should_receive('post').with_args(
f'{default_base_url}/{topic}',
headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL),
headers=return_default_message_headers(borgmatic.hooks.monitoring.monitor.State.FAIL),
auth=None,
).and_return(response)
flexmock(module.logger).should_receive('warning').once()
@@ -287,7 +287,7 @@ def test_ping_monitor_with_other_error_logs_warning():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -1,6 +1,6 @@
from flexmock import flexmock
from borgmatic.hooks import pagerduty as module
from borgmatic.hooks.monitoring import pagerduty as module
def test_ping_monitor_ignores_start_state():
@@ -1,8 +1,8 @@
import pytest
from flexmock import flexmock
import borgmatic.hooks.monitor
from borgmatic.hooks import pushover as module
import borgmatic.hooks.monitoring.monitor
from borgmatic.hooks.monitoring import pushover as module
def test_ping_monitor_config_with_minimum_config_fail_state_backup_successfully_send_to_pushover():
@@ -26,7 +26,7 @@ def test_ping_monitor_config_with_minimum_config_fail_state_backup_successfully_
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -45,7 +45,7 @@ def test_ping_monitor_config_with_minimum_config_start_state_backup_not_send_to_
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.START,
borgmatic.hooks.monitoring.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
)
@@ -78,7 +78,7 @@ def test_ping_monitor_start_state_backup_default_message_successfully_send_to_pu
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.START,
borgmatic.hooks.monitoring.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
)
@@ -111,7 +111,7 @@ def test_ping_monitor_start_state_backup_custom_message_successfully_send_to_pus
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.START,
borgmatic.hooks.monitoring.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
)
@@ -146,7 +146,7 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_emergency
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.START,
borgmatic.hooks.monitoring.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
)
@@ -181,7 +181,7 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_emergency
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.START,
borgmatic.hooks.monitoring.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
)
@@ -216,7 +216,7 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_emergency
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.START,
borgmatic.hooks.monitoring.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
)
@@ -244,7 +244,7 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_high_decl
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.START,
borgmatic.hooks.monitoring.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
)
@@ -307,7 +307,7 @@ def test_ping_monitor_start_state_backup_based_on_documentation_advanced_example
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.START,
borgmatic.hooks.monitoring.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
)
@@ -375,7 +375,7 @@ def test_ping_monitor_fail_state_backup_based_on_documentation_advanced_example_
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -440,7 +440,7 @@ def test_ping_monitor_finish_state_backup_based_on_documentation_advanced_exampl
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FINISH,
borgmatic.hooks.monitoring.monitor.State.FINISH,
monitoring_log_level=1,
dry_run=False,
)
@@ -459,7 +459,7 @@ def test_ping_monitor_config_with_minimum_config_fail_state_backup_successfully_
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=True,
)
@@ -480,7 +480,7 @@ def test_ping_monitor_config_incorrect_state_exit_early():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.START,
borgmatic.hooks.monitoring.monitor.State.START,
monitoring_log_level=1,
dry_run=True,
)
@@ -516,7 +516,7 @@ def test_ping_monitor_push_post_error_exits_early():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -1,7 +1,7 @@
from flexmock import flexmock
import borgmatic.hooks.monitor
from borgmatic.hooks import uptimekuma as module
import borgmatic.hooks.monitoring.monitor
from borgmatic.hooks.monitoring import uptime_kuma as module
DEFAULT_PUSH_URL = 'https://example.uptime.kuma/api/push/abcd1234'
CUSTOM_PUSH_URL = 'https://uptime.example.com/api/push/efgh5678'
@@ -17,7 +17,7 @@ def test_ping_monitor_hits_default_uptimekuma_on_fail():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -33,7 +33,7 @@ def test_ping_monitor_hits_custom_uptimekuma_on_fail():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -49,7 +49,7 @@ def test_ping_monitor_custom_uptimekuma_on_start():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.START,
borgmatic.hooks.monitoring.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
)
@@ -65,7 +65,7 @@ def test_ping_monitor_custom_uptimekuma_on_finish():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FINISH,
borgmatic.hooks.monitoring.monitor.State.FINISH,
monitoring_log_level=1,
dry_run=False,
)
@@ -79,7 +79,7 @@ def test_ping_monitor_does_not_hit_custom_uptimekuma_on_fail_dry_run():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=True,
)
@@ -93,7 +93,7 @@ def test_ping_monitor_does_not_hit_custom_uptimekuma_on_start_dry_run():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.START,
borgmatic.hooks.monitoring.monitor.State.START,
monitoring_log_level=1,
dry_run=True,
)
@@ -107,7 +107,7 @@ def test_ping_monitor_does_not_hit_custom_uptimekuma_on_finish_dry_run():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FINISH,
borgmatic.hooks.monitoring.monitor.State.FINISH,
monitoring_log_level=1,
dry_run=True,
)
@@ -124,7 +124,7 @@ def test_ping_monitor_with_connection_error_logs_warning():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -145,7 +145,7 @@ def test_ping_monitor_with_other_error_logs_warning():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -159,7 +159,7 @@ def test_ping_monitor_with_invalid_run_state():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.LOG,
borgmatic.hooks.monitoring.monitor.State.LOG,
monitoring_log_level=1,
dry_run=True,
)
@@ -1,7 +1,7 @@
from flexmock import flexmock
import borgmatic.hooks.monitor
from borgmatic.hooks import zabbix as module
import borgmatic.hooks.monitoring.monitor
from borgmatic.hooks.monitoring import zabbix as module
SERVER = 'https://zabbix.com/zabbix/api_jsonrpc.php'
ITEMID = 55105
@@ -65,7 +65,7 @@ def test_ping_monitor_with_non_matching_state_exits_early():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.START,
borgmatic.hooks.monitoring.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
)
@@ -82,7 +82,7 @@ def test_ping_monitor_config_with_api_key_only_exit_early():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -99,7 +99,7 @@ def test_ping_monitor_config_with_host_only_exit_early():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -116,7 +116,7 @@ def test_ping_monitor_config_with_key_only_exit_early():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -133,7 +133,7 @@ def test_ping_monitor_config_with_server_only_exit_early():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -149,7 +149,7 @@ def test_ping_monitor_config_user_password_no_zabbix_data_exit_early():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -165,7 +165,7 @@ def test_ping_monitor_config_api_key_no_zabbix_data_exit_early():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -182,7 +182,7 @@ def test_ping_monitor_config_itemid_no_auth_data_exit_early():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -199,7 +199,7 @@ def test_ping_monitor_config_host_and_key_no_auth_data_exit_early():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -220,7 +220,7 @@ def test_ping_monitor_config_host_and_key_with_api_key_auth_data_successful():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -235,7 +235,7 @@ def test_ping_monitor_config_host_and_missing_key_exits_early():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -250,7 +250,7 @@ def test_ping_monitor_config_key_and_missing_host_exits_early():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -290,7 +290,7 @@ def test_ping_monitor_config_host_and_key_with_username_password_auth_data_succe
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -329,7 +329,7 @@ def test_ping_monitor_config_host_and_key_with_username_password_auth_data_and_a
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -350,7 +350,7 @@ def test_ping_monitor_config_host_and_key_with_username_and_missing_password_exi
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -371,7 +371,7 @@ def test_ping_monitor_config_host_and_key_with_passing_and_missing_username_exit
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -392,7 +392,7 @@ def test_ping_monitor_config_itemid_with_api_key_auth_data_successful():
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -426,7 +426,7 @@ def test_ping_monitor_config_itemid_with_username_password_auth_data_successful(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
@@ -462,7 +462,7 @@ def test_ping_monitor_config_itemid_with_username_password_auth_data_and_push_po
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
borgmatic.hooks.monitoring.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
+47 -11
View File
@@ -35,12 +35,32 @@ def test_interpolate_context_escapes_interpolated_variables():
)
def test_make_environment_without_pyinstaller_does_not_touch_environment():
assert module.make_environment({}, sys_module=flexmock()) == {}
def test_make_environment_with_pyinstaller_clears_LD_LIBRARY_PATH():
assert module.make_environment({}, sys_module=flexmock(frozen=True, _MEIPASS='yup')) == {
'LD_LIBRARY_PATH': ''
}
def test_make_environment_with_pyinstaller_and_LD_LIBRARY_PATH_ORIG_copies_it_into_LD_LIBRARY_PATH():
assert module.make_environment(
{'LD_LIBRARY_PATH_ORIG': '/lib/lib/lib'}, sys_module=flexmock(frozen=True, _MEIPASS='yup')
) == {'LD_LIBRARY_PATH': '/lib/lib/lib'}
def test_execute_hook_invokes_each_command():
flexmock(module).should_receive('interpolate_context').replace_with(
lambda config_file, hook_description, command, context: command
)
flexmock(module.execute).should_receive('execute_command').with_args(
[':'], output_log_level=logging.WARNING, shell=True
flexmock(module).should_receive('make_environment').and_return({})
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
[':'],
output_log_level=logging.WARNING,
shell=True,
extra_environment={},
).once()
module.execute_hook([':'], None, 'config.yaml', 'pre-backup', dry_run=False)
@@ -50,11 +70,18 @@ def test_execute_hook_with_multiple_commands_invokes_each_command():
flexmock(module).should_receive('interpolate_context').replace_with(
lambda config_file, hook_description, command, context: command
)
flexmock(module.execute).should_receive('execute_command').with_args(
[':'], output_log_level=logging.WARNING, shell=True
flexmock(module).should_receive('make_environment').and_return({})
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
[':'],
output_log_level=logging.WARNING,
shell=True,
extra_environment={},
).once()
flexmock(module.execute).should_receive('execute_command').with_args(
['true'], output_log_level=logging.WARNING, shell=True
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
['true'],
output_log_level=logging.WARNING,
shell=True,
extra_environment={},
).once()
module.execute_hook([':', 'true'], None, 'config.yaml', 'pre-backup', dry_run=False)
@@ -66,8 +93,12 @@ def test_execute_hook_with_umask_sets_that_umask():
)
flexmock(module.os).should_receive('umask').with_args(0o77).and_return(0o22).once()
flexmock(module.os).should_receive('umask').with_args(0o22).once()
flexmock(module.execute).should_receive('execute_command').with_args(
[':'], output_log_level=logging.WARNING, shell=True
flexmock(module).should_receive('make_environment').and_return({})
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
[':'],
output_log_level=logging.WARNING,
shell=True,
extra_environment={},
)
module.execute_hook([':'], 77, 'config.yaml', 'pre-backup', dry_run=False)
@@ -77,7 +108,8 @@ def test_execute_hook_with_dry_run_skips_commands():
flexmock(module).should_receive('interpolate_context').replace_with(
lambda config_file, hook_description, command, context: command
)
flexmock(module.execute).should_receive('execute_command').never()
flexmock(module).should_receive('make_environment').and_return({})
flexmock(module.borgmatic.execute).should_receive('execute_command').never()
module.execute_hook([':', 'true'], None, 'config.yaml', 'pre-backup', dry_run=True)
@@ -90,8 +122,12 @@ def test_execute_hook_on_error_logs_as_error():
flexmock(module).should_receive('interpolate_context').replace_with(
lambda config_file, hook_description, command, context: command
)
flexmock(module.execute).should_receive('execute_command').with_args(
[':'], output_log_level=logging.ERROR, shell=True
flexmock(module).should_receive('make_environment').and_return({})
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
[':'],
output_log_level=logging.ERROR,
shell=True,
extra_environment={},
).once()
module.execute_hook([':'], None, 'config.yaml', 'on-error', dry_run=False)
+145 -15
View File
@@ -17,7 +17,15 @@ def test_call_hook_invokes_module_function_with_arguments_and_returns_value():
config = {'super_hook': flexmock(), 'other_hook': flexmock()}
expected_return_value = flexmock()
test_module = sys.modules[__name__]
flexmock(module).HOOK_NAME_TO_MODULE = {'super_hook': test_module}
flexmock(module).should_receive('get_submodule_names').with_args(
module.borgmatic.hooks.data_source
).and_return(['other_hook'])
flexmock(module).should_receive('get_submodule_names').with_args(
module.borgmatic.hooks.monitoring
).and_return(['super_hook', 'third_hook'])
flexmock(module.importlib).should_receive('import_module').with_args(
'borgmatic.hooks.monitoring.super_hook'
).and_return(test_module)
flexmock(test_module).should_receive('hook_function').with_args(
config['super_hook'], config, 'prefix', 55, value=66
).and_return(expected_return_value).once()
@@ -27,11 +35,65 @@ def test_call_hook_invokes_module_function_with_arguments_and_returns_value():
assert return_value == expected_return_value
def test_call_hook_probes_config_with_databases_suffix():
config = {'super_hook_databases': flexmock(), 'other_hook': flexmock()}
expected_return_value = flexmock()
test_module = sys.modules[__name__]
flexmock(module).should_receive('get_submodule_names').with_args(
module.borgmatic.hooks.data_source
).and_return(['other_hook'])
flexmock(module).should_receive('get_submodule_names').with_args(
module.borgmatic.hooks.monitoring
).and_return(['super_hook', 'third_hook'])
flexmock(module.importlib).should_receive('import_module').with_args(
'borgmatic.hooks.monitoring.super_hook'
).and_return(test_module)
flexmock(test_module).should_receive('hook_function').with_args(
config['super_hook_databases'], config, 'prefix', 55, value=66
).and_return(expected_return_value).once()
return_value = module.call_hook('hook_function', config, 'prefix', 'super_hook', 55, value=66)
assert return_value == expected_return_value
def test_call_hook_strips_databases_suffix_from_hook_name():
config = {'super_hook_databases': flexmock(), 'other_hook_databases': flexmock()}
expected_return_value = flexmock()
test_module = sys.modules[__name__]
flexmock(module).should_receive('get_submodule_names').with_args(
module.borgmatic.hooks.data_source
).and_return(['other_hook'])
flexmock(module).should_receive('get_submodule_names').with_args(
module.borgmatic.hooks.monitoring
).and_return(['super_hook', 'third_hook'])
flexmock(module.importlib).should_receive('import_module').with_args(
'borgmatic.hooks.monitoring.super_hook'
).and_return(test_module)
flexmock(test_module).should_receive('hook_function').with_args(
config['super_hook_databases'], config, 'prefix', 55, value=66
).and_return(expected_return_value).once()
return_value = module.call_hook(
'hook_function', config, 'prefix', 'super_hook_databases', 55, value=66
)
assert return_value == expected_return_value
def test_call_hook_without_hook_config_invokes_module_function_with_arguments_and_returns_value():
config = {'other_hook': flexmock()}
expected_return_value = flexmock()
test_module = sys.modules[__name__]
flexmock(module).HOOK_NAME_TO_MODULE = {'super_hook': test_module}
flexmock(module).should_receive('get_submodule_names').with_args(
module.borgmatic.hooks.data_source
).and_return(['other_hook'])
flexmock(module).should_receive('get_submodule_names').with_args(
module.borgmatic.hooks.monitoring
).and_return(['super_hook', 'third_hook'])
flexmock(module.importlib).should_receive('import_module').with_args(
'borgmatic.hooks.monitoring.super_hook'
).and_return(test_module)
flexmock(test_module).should_receive('hook_function').with_args(
{}, config, 'prefix', 55, value=66
).and_return(expected_return_value).once()
@@ -44,35 +106,69 @@ def test_call_hook_without_hook_config_invokes_module_function_with_arguments_an
def test_call_hook_without_corresponding_module_raises():
config = {'super_hook': flexmock(), 'other_hook': flexmock()}
test_module = sys.modules[__name__]
flexmock(module).HOOK_NAME_TO_MODULE = {'other_hook': test_module}
flexmock(module).should_receive('get_submodule_names').with_args(
module.borgmatic.hooks.data_source
).and_return(['other_hook'])
flexmock(module).should_receive('get_submodule_names').with_args(
module.borgmatic.hooks.monitoring
).and_return(['some_hook'])
flexmock(module.importlib).should_receive('import_module').with_args(
'borgmatic.hooks.monitoring.super_hook'
).and_return(test_module)
flexmock(test_module).should_receive('hook_function').never()
with pytest.raises(ValueError):
module.call_hook('hook_function', config, 'prefix', 'super_hook', 55, value=66)
def test_call_hook_skips_non_hook_modules():
config = {'not_a_hook': flexmock(), 'other_hook': flexmock()}
flexmock(module).should_receive('get_submodule_names').with_args(
module.borgmatic.hooks.data_source
).and_return(['other_hook'])
flexmock(module).should_receive('get_submodule_names').with_args(
module.borgmatic.hooks.monitoring
).and_return(['not_a_hook', 'third_hook'])
not_a_hook_module = flexmock(IS_A_HOOK=False)
flexmock(module.importlib).should_receive('import_module').with_args(
'borgmatic.hooks.monitoring.not_a_hook'
).and_return(not_a_hook_module)
return_value = module.call_hook('hook_function', config, 'prefix', 'not_a_hook', 55, value=66)
assert return_value is None
def test_call_hooks_calls_each_hook_and_collects_return_values():
config = {'super_hook': flexmock(), 'other_hook': flexmock()}
expected_return_values = {'super_hook': flexmock(), 'other_hook': flexmock()}
flexmock(module.importlib).should_receive('import_module').with_args(
'borgmatic.hooks.monitoring'
).and_return(module.borgmatic.hooks.monitoring)
flexmock(module).should_receive('get_submodule_names').with_args(
module.borgmatic.hooks.monitoring
).and_return(['super_hook', 'other_hook'])
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
)
return_values = module.call_hooks('do_stuff', config, 'prefix', module.Hook_type.MONITORING, 55)
assert return_values == expected_return_values
def test_call_hooks_calls_skips_return_values_for_missing_hooks():
def test_call_hooks_calls_skips_return_values_for_unconfigured_hooks():
config = {'super_hook': flexmock()}
expected_return_values = {'super_hook': flexmock()}
flexmock(module.importlib).should_receive('import_module').with_args(
'borgmatic.hooks.monitoring'
).and_return(module.borgmatic.hooks.monitoring)
flexmock(module).should_receive('get_submodule_names').with_args(
module.borgmatic.hooks.monitoring
).and_return(['super_hook', 'other_hook'])
flexmock(module).should_receive('call_hook').and_return(expected_return_values['super_hook'])
return_values = module.call_hooks(
'do_stuff', config, 'prefix', ('super_hook', 'other_hook'), 55
)
return_values = module.call_hooks('do_stuff', config, 'prefix', module.Hook_type.MONITORING, 55)
assert return_values == expected_return_values
@@ -80,13 +176,35 @@ def test_call_hooks_calls_skips_return_values_for_missing_hooks():
def test_call_hooks_calls_treats_null_hook_as_optionless():
config = {'super_hook': flexmock(), 'other_hook': None}
expected_return_values = {'super_hook': flexmock(), 'other_hook': flexmock()}
flexmock(module.importlib).should_receive('import_module').with_args(
'borgmatic.hooks.monitoring'
).and_return(module.borgmatic.hooks.monitoring)
flexmock(module).should_receive('get_submodule_names').with_args(
module.borgmatic.hooks.monitoring
).and_return(['super_hook', 'other_hook'])
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
)
return_values = module.call_hooks('do_stuff', config, 'prefix', module.Hook_type.MONITORING, 55)
assert return_values == expected_return_values
def test_call_hooks_calls_looks_up_databases_suffix_in_config():
config = {'super_hook_databases': flexmock(), 'other_hook': flexmock()}
expected_return_values = {'super_hook': flexmock(), 'other_hook': flexmock()}
flexmock(module.importlib).should_receive('import_module').with_args(
'borgmatic.hooks.monitoring'
).and_return(module.borgmatic.hooks.monitoring)
flexmock(module).should_receive('get_submodule_names').with_args(
module.borgmatic.hooks.monitoring
).and_return(['super_hook', 'other_hook'])
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', module.Hook_type.MONITORING, 55)
assert return_values == expected_return_values
@@ -94,12 +212,18 @@ def test_call_hooks_calls_treats_null_hook_as_optionless():
def test_call_hooks_even_if_unconfigured_calls_each_hook_and_collects_return_values():
config = {'super_hook': flexmock(), 'other_hook': flexmock()}
expected_return_values = {'super_hook': flexmock(), 'other_hook': flexmock()}
flexmock(module.importlib).should_receive('import_module').with_args(
'borgmatic.hooks.monitoring'
).and_return(module.borgmatic.hooks.monitoring)
flexmock(module).should_receive('get_submodule_names').with_args(
module.borgmatic.hooks.monitoring
).and_return(['super_hook', 'other_hook'])
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_even_if_unconfigured(
'do_stuff', config, 'prefix', ('super_hook', 'other_hook'), 55
'do_stuff', config, 'prefix', module.Hook_type.MONITORING, 55
)
assert return_values == expected_return_values
@@ -108,12 +232,18 @@ def test_call_hooks_even_if_unconfigured_calls_each_hook_and_collects_return_val
def test_call_hooks_even_if_unconfigured_calls_each_hook_configured_or_not_and_collects_return_values():
config = {'other_hook': flexmock()}
expected_return_values = {'super_hook': flexmock(), 'other_hook': flexmock()}
flexmock(module.importlib).should_receive('import_module').with_args(
'borgmatic.hooks.monitoring'
).and_return(module.borgmatic.hooks.monitoring)
flexmock(module).should_receive('get_submodule_names').with_args(
module.borgmatic.hooks.monitoring
).and_return(['super_hook', 'other_hook'])
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_even_if_unconfigured(
'do_stuff', config, 'prefix', ('super_hook', 'other_hook'), 55
'do_stuff', config, 'prefix', module.Hook_type.MONITORING, 55
)
assert return_values == expected_return_values