mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-23 18:33:02 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81cd03cbbf | ||
|
|
f2455527fc | ||
|
|
62d67cde0a | ||
|
|
ae8a9db27d | ||
|
|
8979f8918d | ||
|
|
eb97708092 | ||
|
|
f2d93b85b4 | ||
|
|
b999d2dc4d | ||
|
|
7f2e38d061 | ||
|
|
140fc248b6 | ||
|
|
ec9e1a8223 | ||
|
|
03bbe77dd9 | ||
|
|
f1c5f11422 | ||
|
|
f8df06fb92 | ||
|
|
d95707ff9b | ||
|
|
51a7f50e3a | ||
|
|
49b8b693af | ||
|
|
d0e92493f6 | ||
|
|
9afdaca985 | ||
|
|
cc11ed78e0 | ||
|
|
87f3746881 | ||
|
|
347a4c3dd5 | ||
|
|
399bb6ef68 | ||
|
|
9b9ecad299 | ||
|
|
8c4b899a13 | ||
|
|
9b77de3d66 | ||
|
|
bfeea5d394 | ||
|
|
8a6225b7c2 | ||
|
|
9aaa3c925f | ||
|
|
88fd1ae454 | ||
|
|
27305ec2bf | ||
|
|
4453c2d49c | ||
|
|
6367a00013 | ||
|
|
cd654cbb57 | ||
|
|
1e8f73779f | ||
|
|
27d167b071 | ||
|
|
cfff6c6855 | ||
|
|
37efaeae88 | ||
|
|
0978c669ad | ||
|
|
1366269586 | ||
|
|
a9a0910817 | ||
|
|
5bcc7b60c8 | ||
|
|
84a0552277 | ||
|
|
d4a02f73b5 | ||
|
|
3f901c0a52 | ||
|
|
b5b5c1fafa | ||
|
|
86e5085acc | ||
|
|
08a5e8717b | ||
|
|
6b2f2b2ac4 | ||
|
|
a07cf9e699 | ||
|
|
bf40b01077 | ||
|
|
a5c6a2fe1c |
@@ -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/
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
@@ -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__)
|
||||
|
||||
@@ -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
@@ -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}')
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "borgmatic"
|
||||
version = "1.9.3"
|
||||
version = "1.9.4"
|
||||
authors = [
|
||||
{ name="Dan Helfman", email="witten@torsion.org" },
|
||||
]
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
+3
-3
@@ -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
|
||||
)
|
||||
)
|
||||
+3
-3
@@ -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
|
||||
)
|
||||
)
|
||||
+1
-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():
|
||||
@@ -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',
|
||||
|
||||
@@ -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():
|
||||
@@ -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():
|
||||
+1
-1
@@ -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():
|
||||
+54
-30
@@ -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,
|
||||
)
|
||||
+12
-12
@@ -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,
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user