mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-24 10:53:02 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87e77ff2b7 | ||
|
|
3517d9d4f3 | ||
|
|
d3c7279dad | ||
|
|
a99c48c115 | ||
|
|
94cedd4cf8 | ||
|
|
a4baf4623b | ||
|
|
77df425bd1 | ||
|
|
69476a4fab | ||
|
|
be6b865a81 | ||
|
|
b58a52e03f | ||
|
|
9b85c5bc61 | ||
|
|
b8041f5c39 | ||
|
|
d9d6d3f7f2 | ||
|
|
0844cd0d4f | ||
|
|
d4705602fa | ||
|
|
5174a78109 | ||
|
|
3db79b4352 | ||
|
|
d6732d9abb | ||
|
|
267af5b372 | ||
|
|
d53ea09adb | ||
|
|
8696cbfa22 | ||
|
|
48dca28c74 | ||
|
|
36bcbd0592 | ||
|
|
ebb3bca4b3 | ||
|
|
b1e343f15c | ||
|
|
cb7f98192c | ||
|
|
3ceb4f554f | ||
|
|
4b18c0bc81 | ||
|
|
2ce09dbf82 | ||
|
|
8a4f3b8f1a | ||
|
|
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,26 @@
|
||||
1.9.5
|
||||
* #418: Backup and restore databases that have the same name but with different ports, hostnames,
|
||||
or hooks.
|
||||
* #947: To avoid a hang in the database hooks, error and exit when the borgmatic runtime
|
||||
directory overlaps with the configured excludes.
|
||||
* #954: Fix a findmnt command error in the Btrfs hook by switching to parsing JSON output.
|
||||
* #956: Fix the printing of a color reset code even when color is disabled.
|
||||
* #958: Drop colorama as a library dependency.
|
||||
* When the ZFS, Btrfs, or LVM hooks aren't configured, don't try to cleanup snapshots for them.
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
+241
-178
@@ -1,4 +1,4 @@
|
||||
import copy
|
||||
import collections
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
@@ -11,58 +11,96 @@ 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__)
|
||||
|
||||
|
||||
UNSPECIFIED_HOOK = object()
|
||||
UNSPECIFIED = object()
|
||||
|
||||
|
||||
def get_configured_data_source(
|
||||
config,
|
||||
archive_data_source_names,
|
||||
hook_name,
|
||||
data_source_name,
|
||||
configuration_data_source_name=None,
|
||||
):
|
||||
Dump = collections.namedtuple(
|
||||
'Dump',
|
||||
('hook_name', 'data_source_name', 'hostname', 'port'),
|
||||
defaults=('localhost', None),
|
||||
)
|
||||
|
||||
|
||||
def dumps_match(first, second):
|
||||
'''
|
||||
Find the first data source with the given hook name and data source name in the configuration
|
||||
dict and the given archive data source names dict (from hook name to data source names contained
|
||||
in a particular backup archive). If UNSPECIFIED_HOOK is given as the hook name, search all data
|
||||
source hooks for the named data source. If a configuration data source name is given, use that
|
||||
instead of the data source name to lookup the data source in the given hooks configuration.
|
||||
|
||||
Return the found data source as a tuple of (found hook name, data source configuration dict) or
|
||||
(None, None) if not found.
|
||||
Compare two Dump instances for equality while supporting a field value of UNSPECIFIED, which
|
||||
indicates that the field should match any value.
|
||||
'''
|
||||
if not configuration_data_source_name:
|
||||
configuration_data_source_name = data_source_name
|
||||
for field_name in first._fields:
|
||||
first_value = getattr(first, field_name)
|
||||
second_value = getattr(second, field_name)
|
||||
|
||||
if hook_name == UNSPECIFIED_HOOK:
|
||||
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 first_value == UNSPECIFIED or second_value == UNSPECIFIED:
|
||||
continue
|
||||
|
||||
if first_value != second_value:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def render_dump_metadata(dump):
|
||||
'''
|
||||
Given a Dump instance, make a display string describing it for use in log messages.
|
||||
'''
|
||||
name = 'unspecified' if dump.data_source_name is UNSPECIFIED else dump.data_source_name
|
||||
hostname = dump.hostname or 'localhost'
|
||||
port = None if dump.port is UNSPECIFIED else dump.port
|
||||
|
||||
if port:
|
||||
metadata = f'{name}@:{port}' if hostname is UNSPECIFIED else f'{name}@{hostname}:{port}'
|
||||
else:
|
||||
try:
|
||||
hooks_to_search = {hook_name: config[hook_name]}
|
||||
except KeyError:
|
||||
return (None, None)
|
||||
metadata = f'{name}' if hostname is UNSPECIFIED else f'{name}@{hostname}'
|
||||
|
||||
return next(
|
||||
(
|
||||
(name, hook_data_source)
|
||||
for (name, hook) in hooks_to_search.items()
|
||||
for hook_data_source in hook
|
||||
if hook_data_source['name'] == configuration_data_source_name
|
||||
and data_source_name in archive_data_source_names.get(name, [])
|
||||
),
|
||||
(None, None),
|
||||
if dump.hook_name not in (None, UNSPECIFIED):
|
||||
return f'{metadata} ({dump.hook_name})'
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
def get_configured_data_source(config, restore_dump):
|
||||
'''
|
||||
Search in the given configuration dict for dumps corresponding to the given dump to restore. If
|
||||
there are multiple matches, error.
|
||||
|
||||
Return the found data source as a data source configuration dict or None if not found.
|
||||
'''
|
||||
try:
|
||||
hooks_to_search = {restore_dump.hook_name: config[restore_dump.hook_name]}
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
matching_dumps = tuple(
|
||||
hook_data_source
|
||||
for (hook_name, hook) in hooks_to_search.items()
|
||||
for hook_data_source in hook
|
||||
if dumps_match(
|
||||
Dump(
|
||||
hook_name,
|
||||
hook_data_source.get('name'),
|
||||
hook_data_source.get('hostname', 'localhost'),
|
||||
hook_data_source.get('port'),
|
||||
),
|
||||
restore_dump,
|
||||
)
|
||||
)
|
||||
|
||||
if not matching_dumps:
|
||||
return None
|
||||
|
||||
if len(matching_dumps) > 1:
|
||||
raise ValueError(
|
||||
f'Cannot restore data source {render_dump_metadata(restore_dump)} because there are multiple matching data sources configured'
|
||||
)
|
||||
|
||||
return matching_dumps[0]
|
||||
|
||||
|
||||
def strip_path_prefix_from_extracted_dump_destination(
|
||||
destination_path, borgmatic_runtime_directory
|
||||
@@ -97,7 +135,7 @@ def strip_path_prefix_from_extracted_dump_destination(
|
||||
break
|
||||
|
||||
|
||||
def restore_single_data_source(
|
||||
def restore_single_dump(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
@@ -115,18 +153,22 @@ def restore_single_data_source(
|
||||
username/password as connection params, and a configured data source configuration dict, restore
|
||||
that data source from the archive.
|
||||
'''
|
||||
dump_metadata = render_dump_metadata(
|
||||
Dump(hook_name, data_source['name'], data_source.get('hostname'), data_source.get('port'))
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f'{repository.get("label", repository["path"])}: Restoring data source {data_source["name"]}'
|
||||
f'{repository.get("label", repository["path"])}: Restoring data source {dump_metadata}'
|
||||
)
|
||||
|
||||
dump_patterns = borgmatic.hooks.dispatch.call_hooks(
|
||||
'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', 1)[0]]
|
||||
|
||||
destination_path = (
|
||||
tempfile.mkdtemp(dir=borgmatic_runtime_directory)
|
||||
@@ -141,7 +183,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 +208,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,
|
||||
@@ -175,7 +221,7 @@ def restore_single_data_source(
|
||||
)
|
||||
|
||||
|
||||
def collect_archive_data_source_names(
|
||||
def collect_dumps_from_archive(
|
||||
repository,
|
||||
archive,
|
||||
config,
|
||||
@@ -187,17 +233,17 @@ def collect_archive_data_source_names(
|
||||
):
|
||||
'''
|
||||
Given a local or remote repository path, a resolved archive name, a configuration dict, the
|
||||
local Borg version, global_arguments an argparse.Namespace, local and remote Borg paths, and the
|
||||
borgmatic runtime directory, query the archive for the names of data sources it contains as
|
||||
dumps and return them as a dict from hook name to a sequence of data source names.
|
||||
local Borg version, global arguments an argparse.Namespace, local and remote Borg paths, and the
|
||||
borgmatic runtime directory, query the archive for the names of data sources dumps it contains
|
||||
and return them as a set of Dump instances.
|
||||
'''
|
||||
borgmatic_source_directory = str(
|
||||
pathlib.Path(borgmatic.config.paths.get_borgmatic_source_directory(config))
|
||||
)
|
||||
|
||||
# Probe for the data source dumps in multiple locations, as the default location has moved to
|
||||
# the borgmatic runtime directory (which get stored as just "/borgmatic" with Borg 1.4+). But we
|
||||
# still want to support reading dumps from previously created archives as well.
|
||||
# the borgmatic runtime directory (which gets stored as just "/borgmatic" with Borg 1.4+). But
|
||||
# we still want to support reading dumps from previously created archives as well.
|
||||
dump_paths = borgmatic.borg.list.capture_archive_listing(
|
||||
repository,
|
||||
archive,
|
||||
@@ -206,7 +252,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),
|
||||
@@ -217,110 +265,145 @@ def collect_archive_data_source_names(
|
||||
remote_path=remote_path,
|
||||
)
|
||||
|
||||
# Determine the data source names corresponding to the dumps found in the archive and
|
||||
# add them to restore_names.
|
||||
archive_data_source_names = {}
|
||||
# Parse the paths of dumps found in the archive to get their respective dump metadata.
|
||||
dumps_from_archive = set()
|
||||
|
||||
for dump_path in dump_paths:
|
||||
if not dump_path:
|
||||
continue
|
||||
|
||||
# Probe to find the base directory that's at the start of the dump path.
|
||||
for base_directory in (
|
||||
'borgmatic',
|
||||
borgmatic_runtime_directory,
|
||||
borgmatic_source_directory,
|
||||
):
|
||||
try:
|
||||
(hook_name, _, data_source_name) = dump_path.split(base_directory + os.path.sep, 1)[
|
||||
1
|
||||
].split(os.path.sep)[0:3]
|
||||
(hook_name, host_and_port, data_source_name) = dump_path.split(
|
||||
base_directory + os.path.sep, 1
|
||||
)[1].split(os.path.sep)[0:3]
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
else:
|
||||
if data_source_name not in archive_data_source_names.get(hook_name, []):
|
||||
archive_data_source_names.setdefault(hook_name, []).extend([data_source_name])
|
||||
break
|
||||
continue
|
||||
|
||||
parts = host_and_port.split(':', 1)
|
||||
|
||||
if len(parts) == 1:
|
||||
parts += (None,)
|
||||
|
||||
(hostname, port) = parts
|
||||
|
||||
try:
|
||||
port = int(port)
|
||||
except (ValueError, TypeError):
|
||||
port = None
|
||||
|
||||
dumps_from_archive.add(Dump(hook_name, data_source_name, hostname, port))
|
||||
|
||||
# We've successfully parsed the dump path, so need to probe any further.
|
||||
break
|
||||
else:
|
||||
logger.warning(
|
||||
f'{repository}: Ignoring invalid data source dump path "{dump_path}" in archive {archive}'
|
||||
)
|
||||
|
||||
return archive_data_source_names
|
||||
return dumps_from_archive
|
||||
|
||||
|
||||
def find_data_sources_to_restore(requested_data_source_names, archive_data_source_names):
|
||||
def get_dumps_to_restore(restore_arguments, dumps_from_archive):
|
||||
'''
|
||||
Given a sequence of requested data source names to restore and a dict of hook name to the names
|
||||
of data sources found in an archive, return an expanded sequence of data source names to
|
||||
restore, replacing "all" with actual data source names as appropriate.
|
||||
Given restore arguments as an argparse.Namespace instance indicating which dumps to restore and
|
||||
a set of Dump instances representing the dumps found in an archive, return a set of specific
|
||||
Dump instances from the archive to restore. As part of this, replace any Dump having a data
|
||||
source name of "all" with multiple named Dump instances as appropriate.
|
||||
|
||||
Raise ValueError if any of the requested data source names cannot be found in the archive.
|
||||
Raise ValueError if any of the requested data source names cannot be found in the archive or if
|
||||
there are multiple archive dump matches for a given requested dump.
|
||||
'''
|
||||
# A map from data source hook name to the data source names to restore for that hook.
|
||||
restore_names = (
|
||||
{UNSPECIFIED_HOOK: requested_data_source_names}
|
||||
if requested_data_source_names
|
||||
else {UNSPECIFIED_HOOK: ['all']}
|
||||
requested_dumps = (
|
||||
{
|
||||
Dump(
|
||||
hook_name=(
|
||||
(
|
||||
restore_arguments.hook
|
||||
if restore_arguments.hook.endswith('_databases')
|
||||
else f'{restore_arguments.hook}_databases'
|
||||
)
|
||||
if restore_arguments.hook
|
||||
else UNSPECIFIED
|
||||
),
|
||||
data_source_name=name,
|
||||
hostname=restore_arguments.original_hostname or 'localhost',
|
||||
port=restore_arguments.original_port,
|
||||
)
|
||||
for name in restore_arguments.data_sources
|
||||
}
|
||||
if restore_arguments.data_sources
|
||||
else {
|
||||
Dump(
|
||||
hook_name=UNSPECIFIED,
|
||||
data_source_name='all',
|
||||
hostname=UNSPECIFIED,
|
||||
port=UNSPECIFIED,
|
||||
)
|
||||
}
|
||||
)
|
||||
missing_dumps = set()
|
||||
dumps_to_restore = set()
|
||||
|
||||
# If "all" is in restore_names, then replace it with the names of dumps found within the
|
||||
# archive.
|
||||
if 'all' in restore_names[UNSPECIFIED_HOOK]:
|
||||
restore_names[UNSPECIFIED_HOOK].remove('all')
|
||||
# If there's a requested "all" dump, add every dump from the archive to the dumps to restore.
|
||||
if any(dump for dump in requested_dumps if dump.data_source_name == 'all'):
|
||||
dumps_to_restore.update(dumps_from_archive)
|
||||
|
||||
for hook_name, data_source_names in archive_data_source_names.items():
|
||||
restore_names.setdefault(hook_name, []).extend(data_source_names)
|
||||
# If any archive dump matches a requested dump, add the archive dump to the dumps to restore.
|
||||
for requested_dump in requested_dumps:
|
||||
if requested_dump.data_source_name == 'all':
|
||||
continue
|
||||
|
||||
# If a data source is to be restored as part of "all", then remove it from restore names
|
||||
# so it doesn't get restored twice.
|
||||
for data_source_name in data_source_names:
|
||||
if data_source_name in restore_names[UNSPECIFIED_HOOK]:
|
||||
restore_names[UNSPECIFIED_HOOK].remove(data_source_name)
|
||||
|
||||
if not restore_names[UNSPECIFIED_HOOK]:
|
||||
restore_names.pop(UNSPECIFIED_HOOK)
|
||||
|
||||
combined_restore_names = set(
|
||||
name for data_source_names in restore_names.values() for name in data_source_names
|
||||
)
|
||||
combined_archive_data_source_names = set(
|
||||
name
|
||||
for data_source_names in archive_data_source_names.values()
|
||||
for name in data_source_names
|
||||
)
|
||||
|
||||
missing_names = sorted(set(combined_restore_names) - combined_archive_data_source_names)
|
||||
if missing_names:
|
||||
joined_names = ', '.join(f'"{name}"' for name in missing_names)
|
||||
raise ValueError(
|
||||
f"Cannot restore data source{'s' if len(missing_names) > 1 else ''} {joined_names} missing from archive"
|
||||
matching_dumps = tuple(
|
||||
archive_dump
|
||||
for archive_dump in dumps_from_archive
|
||||
if dumps_match(requested_dump, archive_dump)
|
||||
)
|
||||
|
||||
return restore_names
|
||||
if len(matching_dumps) == 0:
|
||||
missing_dumps.add(requested_dump)
|
||||
elif len(matching_dumps) == 1:
|
||||
dumps_to_restore.add(matching_dumps[0])
|
||||
else:
|
||||
raise ValueError(
|
||||
f'Cannot restore data source {render_dump_metadata(requested_dump)} because there are multiple matching dumps in the archive. Try adding flags to disambiguate.'
|
||||
)
|
||||
|
||||
if missing_dumps:
|
||||
rendered_dumps = ', '.join(
|
||||
f'{render_dump_metadata(dump)}' for dump in sorted(missing_dumps)
|
||||
)
|
||||
|
||||
raise ValueError(
|
||||
f"Cannot restore data source dump{'s' if len(missing_dumps) > 1 else ''} {rendered_dumps} missing from archive"
|
||||
)
|
||||
|
||||
return dumps_to_restore
|
||||
|
||||
|
||||
def ensure_data_sources_found(restore_names, remaining_restore_names, found_names):
|
||||
def ensure_requested_dumps_restored(dumps_to_restore, dumps_actually_restored):
|
||||
'''
|
||||
Given a dict from hook name to data source names to restore, a dict from hook name to remaining
|
||||
data source names to restore, and a sequence of found (actually restored) data source names,
|
||||
raise ValueError if requested data source to restore were missing from the archive and/or
|
||||
Given a set of requested dumps to restore and a set of dumps actually restored, raise ValueError
|
||||
if any requested dumps to restore weren't restored, indicating that they were missing from the
|
||||
configuration.
|
||||
'''
|
||||
combined_restore_names = set(
|
||||
name
|
||||
for data_source_names in tuple(restore_names.values())
|
||||
+ tuple(remaining_restore_names.values())
|
||||
for name in data_source_names
|
||||
)
|
||||
|
||||
if not combined_restore_names and not found_names:
|
||||
if not dumps_actually_restored:
|
||||
raise ValueError('No data source dumps were found to restore')
|
||||
|
||||
missing_names = sorted(set(combined_restore_names) - set(found_names))
|
||||
if missing_names:
|
||||
joined_names = ', '.join(f'"{name}"' for name in missing_names)
|
||||
missing_dumps = sorted(
|
||||
dumps_to_restore - dumps_actually_restored, key=lambda dump: dump.data_source_name
|
||||
)
|
||||
|
||||
if missing_dumps:
|
||||
rendered_dumps = ', '.join(f'{render_dump_metadata(dump)}' for dump in missing_dumps)
|
||||
|
||||
raise ValueError(
|
||||
f"Cannot restore data source{'s' if len(missing_names) > 1 else ''} {joined_names} missing from borgmatic's configuration"
|
||||
f"Cannot restore data source{'s' if len(missing_dumps) > 1 else ''} {rendered_dumps} missing from borgmatic's configuration"
|
||||
)
|
||||
|
||||
|
||||
@@ -337,7 +420,8 @@ def run_restore(
|
||||
Run the "restore" action for the given repository, but only if the repository matches the
|
||||
requested repository in restore arguments.
|
||||
|
||||
Raise ValueError if a configured data source could not be found to restore.
|
||||
Raise ValueError if a configured data source could not be found to restore or there's no
|
||||
matching dump in the archive.
|
||||
'''
|
||||
if restore_arguments.repository and not borgmatic.config.validate.repositories_match(
|
||||
repository, restore_arguments.repository
|
||||
@@ -354,7 +438,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,
|
||||
)
|
||||
@@ -368,7 +452,7 @@ def run_restore(
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
archive_data_source_names = collect_archive_data_source_names(
|
||||
dumps_from_archive = collect_dumps_from_archive(
|
||||
repository['path'],
|
||||
archive_name,
|
||||
config,
|
||||
@@ -378,11 +462,9 @@ def run_restore(
|
||||
remote_path,
|
||||
borgmatic_runtime_directory,
|
||||
)
|
||||
restore_names = find_data_sources_to_restore(
|
||||
restore_arguments.data_sources, archive_data_source_names
|
||||
)
|
||||
found_names = set()
|
||||
remaining_restore_names = {}
|
||||
dumps_to_restore = get_dumps_to_restore(restore_arguments, dumps_from_archive)
|
||||
|
||||
dumps_actually_restored = set()
|
||||
connection_params = {
|
||||
'hostname': restore_arguments.hostname,
|
||||
'port': restore_arguments.port,
|
||||
@@ -391,69 +473,50 @@ def run_restore(
|
||||
'restore_path': restore_arguments.restore_path,
|
||||
}
|
||||
|
||||
for hook_name, data_source_names in restore_names.items():
|
||||
for data_source_name in data_source_names:
|
||||
found_hook_name, found_data_source = get_configured_data_source(
|
||||
config, archive_data_source_names, hook_name, data_source_name
|
||||
)
|
||||
# Restore each dump.
|
||||
for restore_dump in dumps_to_restore:
|
||||
found_data_source = get_configured_data_source(
|
||||
config,
|
||||
restore_dump,
|
||||
)
|
||||
|
||||
if not found_data_source:
|
||||
remaining_restore_names.setdefault(found_hook_name or hook_name, []).append(
|
||||
data_source_name
|
||||
)
|
||||
continue
|
||||
|
||||
found_names.add(data_source_name)
|
||||
restore_single_data_source(
|
||||
repository,
|
||||
# For a dump that wasn't found via an exact match in the configuration, try to fallback
|
||||
# to an "all" data source.
|
||||
if not found_data_source:
|
||||
found_data_source = get_configured_data_source(
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
archive_name,
|
||||
found_hook_name or hook_name,
|
||||
dict(found_data_source, **{'schemas': restore_arguments.schemas}),
|
||||
connection_params,
|
||||
borgmatic_runtime_directory,
|
||||
)
|
||||
|
||||
# For any data sources that weren't found via exact matches in the configuration, try to
|
||||
# fallback to "all" entries.
|
||||
for hook_name, data_source_names in remaining_restore_names.items():
|
||||
for data_source_name in data_source_names:
|
||||
found_hook_name, found_data_source = get_configured_data_source(
|
||||
config, archive_data_source_names, hook_name, data_source_name, 'all'
|
||||
Dump(restore_dump.hook_name, 'all', restore_dump.hostname, restore_dump.port),
|
||||
)
|
||||
|
||||
if not found_data_source:
|
||||
continue
|
||||
|
||||
found_names.add(data_source_name)
|
||||
data_source = copy.copy(found_data_source)
|
||||
data_source['name'] = data_source_name
|
||||
found_data_source = dict(found_data_source)
|
||||
found_data_source['name'] = restore_dump.data_source_name
|
||||
|
||||
restore_single_data_source(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
archive_name,
|
||||
found_hook_name or hook_name,
|
||||
dict(data_source, **{'schemas': restore_arguments.schemas}),
|
||||
connection_params,
|
||||
borgmatic_runtime_directory,
|
||||
)
|
||||
dumps_actually_restored.add(restore_dump)
|
||||
|
||||
restore_single_dump(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
archive_name,
|
||||
restore_dump.hook_name,
|
||||
dict(found_data_source, **{'schemas': restore_arguments.schemas}),
|
||||
connection_params,
|
||||
borgmatic_runtime_directory,
|
||||
)
|
||||
|
||||
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||
'remove_data_source_dumps',
|
||||
config,
|
||||
repository['path'],
|
||||
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
|
||||
borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
|
||||
borgmatic_runtime_directory,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
|
||||
ensure_data_sources_found(restore_names, remaining_restore_names, found_names)
|
||||
ensure_requested_dumps_restored(dumps_to_restore, dumps_actually_restored)
|
||||
|
||||
+28
-13
@@ -160,14 +160,24 @@ def any_parent_directories(path, candidate_parents):
|
||||
|
||||
|
||||
def collect_special_file_paths(
|
||||
create_command, config, local_path, working_directory, borg_environment, skip_directories
|
||||
create_command,
|
||||
config,
|
||||
local_path,
|
||||
working_directory,
|
||||
borg_environment,
|
||||
borgmatic_runtime_directory,
|
||||
):
|
||||
'''
|
||||
Given a Borg create command as a tuple, a configuration dict, a local Borg path, a working
|
||||
directory, a dict of environment variables to pass to Borg, and a sequence of parent directories
|
||||
to skip, collect the paths for any special files (character devices, block devices, and named
|
||||
pipes / FIFOs) that Borg would encounter during a create. These are all paths that could cause
|
||||
Borg to hang if its --read-special flag is used.
|
||||
directory, a dict of environment variables to pass to Borg, and the borgmatic runtime directory,
|
||||
collect the paths for any special files (character devices, block devices, and named pipes /
|
||||
FIFOs) that Borg would encounter during a create. These are all paths that could cause Borg to
|
||||
hang if its --read-special flag is used.
|
||||
|
||||
Skip looking for special files in the given borgmatic runtime directory, as borgmatic creates
|
||||
its own special files there for database dumps. And if the borgmatic runtime directory is
|
||||
configured to be excluded from the files Borg backs up, error, because this means Borg won't be
|
||||
able to consume any database dumps and therefore borgmatic will hang.
|
||||
'''
|
||||
# Omit "--exclude-nodump" from the Borg dry run command, because that flag causes Borg to open
|
||||
# files including any named pipe we've created.
|
||||
@@ -186,12 +196,19 @@ def collect_special_file_paths(
|
||||
for path_line in paths_output.split('\n')
|
||||
if path_line and path_line.startswith('- ') or path_line.startswith('+ ')
|
||||
)
|
||||
skip_paths = {}
|
||||
|
||||
return tuple(
|
||||
path
|
||||
for path in paths
|
||||
if special_file(path) and not any_parent_directories(path, skip_directories)
|
||||
)
|
||||
if os.path.exists(borgmatic_runtime_directory):
|
||||
skip_paths = {
|
||||
path for path in paths if any_parent_directories(path, (borgmatic_runtime_directory,))
|
||||
}
|
||||
|
||||
if not skip_paths:
|
||||
raise ValueError(
|
||||
f'The runtime directory {os.path.normpath(borgmatic_runtime_directory)} overlaps with the configured excludes. Please remove it from excludes or change the runtime directory.'
|
||||
)
|
||||
|
||||
return tuple(path for path in paths if special_file(path) if path not in skip_paths)
|
||||
|
||||
|
||||
def check_all_source_directories_exist(source_directories):
|
||||
@@ -335,9 +352,7 @@ def make_base_create_command(
|
||||
local_path,
|
||||
working_directory,
|
||||
borg_environment,
|
||||
skip_directories=(
|
||||
[borgmatic_runtime_directory] if os.path.exists(borgmatic_runtime_directory) else []
|
||||
),
|
||||
borgmatic_runtime_directory=borgmatic_runtime_directory,
|
||||
)
|
||||
|
||||
if special_file_paths:
|
||||
|
||||
@@ -1153,7 +1153,7 @@ def make_parsers():
|
||||
metavar='NAME',
|
||||
dest='data_sources',
|
||||
action='append',
|
||||
help="Name of data source (e.g. database) to restore from archive, must be defined in borgmatic's configuration, can specify flag multiple times, defaults to all data sources in the archive",
|
||||
help="Name of data source (e.g. database) to restore from the archive, must be defined in borgmatic's configuration, can specify the flag multiple times, defaults to all data sources in the archive",
|
||||
)
|
||||
restore_group.add_argument(
|
||||
'--schema',
|
||||
@@ -1182,6 +1182,19 @@ def make_parsers():
|
||||
'--restore-path',
|
||||
help='Path to restore SQLite database dumps to. Defaults to the "restore_path" option in borgmatic\'s configuration',
|
||||
)
|
||||
restore_group.add_argument(
|
||||
'--original-hostname',
|
||||
help='The hostname where the dump to restore came from, only necessary if you need to disambiguate dumps',
|
||||
)
|
||||
restore_group.add_argument(
|
||||
'--original-port',
|
||||
type=int,
|
||||
help="The port where the dump to restore came from (if that port is in borgmatic's configuration), only necessary if you need to disambiguate dumps",
|
||||
)
|
||||
restore_group.add_argument(
|
||||
'--hook',
|
||||
help='The name of the data source hook for the dump to restore, only necessary if you need to disambiguate dumps',
|
||||
)
|
||||
restore_group.add_argument(
|
||||
'-h', '--help', action='help', help='Show this help message and exit'
|
||||
)
|
||||
|
||||
@@ -8,8 +8,6 @@ import time
|
||||
from queue import Queue
|
||||
from subprocess import CalledProcessError
|
||||
|
||||
import colorama
|
||||
|
||||
import borgmatic.actions.borg
|
||||
import borgmatic.actions.break_lock
|
||||
import borgmatic.actions.change_passphrase
|
||||
@@ -39,7 +37,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 +102,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 +111,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 +187,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 +204,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 +213,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 +240,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 +249,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,
|
||||
)
|
||||
@@ -914,7 +913,7 @@ def main(extra_summary_logs=[]): # pragma: no cover
|
||||
getattr(sub_arguments, 'json', False) for sub_arguments in arguments.values()
|
||||
)
|
||||
color_enabled = should_do_markup(global_arguments.no_color or any_json_flags, configs)
|
||||
colorama.init(autoreset=color_enabled, strip=not color_enabled)
|
||||
|
||||
try:
|
||||
configure_logging(
|
||||
verbosity_to_log_level(global_arguments.verbosity),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -68,9 +68,7 @@ properties:
|
||||
type: boolean
|
||||
description: |
|
||||
Stay in same file system; do not cross mount points beyond the given
|
||||
source directories. Defaults to false. But when a database hook is
|
||||
used, the setting here is ignored and one_file_system is considered
|
||||
true.
|
||||
source directories. Defaults to false.
|
||||
example: true
|
||||
numeric_ids:
|
||||
type: boolean
|
||||
@@ -963,8 +961,8 @@ properties:
|
||||
dump all databases on the host. (Also set the "format"
|
||||
to dump each database to a separate file instead of one
|
||||
combined file.) Note that using this database hook
|
||||
implicitly enables both read_special and one_file_system
|
||||
(see above) to support dump and restore streaming.
|
||||
implicitly enables read_special (see above) to support
|
||||
dump and restore streaming.
|
||||
example: users
|
||||
hostname:
|
||||
type: string
|
||||
@@ -1145,9 +1143,8 @@ properties:
|
||||
description: |
|
||||
Database name (required if using this hook). Or "all" to
|
||||
dump all databases on the host. Note that using this
|
||||
database hook implicitly enables both read_special and
|
||||
one_file_system (see above) to support dump and restore
|
||||
streaming.
|
||||
database hook implicitly enables read_special (see
|
||||
above) to support dump and restore streaming.
|
||||
example: users
|
||||
hostname:
|
||||
type: string
|
||||
@@ -1272,9 +1269,8 @@ properties:
|
||||
description: |
|
||||
Database name (required if using this hook). Or "all" to
|
||||
dump all databases on the host. Note that using this
|
||||
database hook implicitly enables both read_special and
|
||||
one_file_system (see above) to support dump and restore
|
||||
streaming.
|
||||
database hook implicitly enables read_special (see
|
||||
above) to support dump and restore streaming.
|
||||
example: users
|
||||
hostname:
|
||||
type: string
|
||||
@@ -1407,9 +1403,9 @@ properties:
|
||||
description: |
|
||||
Path to the SQLite database file to dump. If relative,
|
||||
it is relative to the current working directory. Note
|
||||
that using this database hook implicitly enables both
|
||||
read_special and one_file_system (see above) to support
|
||||
dump and restore streaming.
|
||||
that using this database hook implicitly enables
|
||||
read_special (see above) to support dump and restore
|
||||
streaming.
|
||||
example: /var/lib/sqlite/users.db
|
||||
restore_path:
|
||||
type: string
|
||||
@@ -1429,9 +1425,8 @@ properties:
|
||||
description: |
|
||||
Database name (required if using this hook). Or "all" to
|
||||
dump all databases on the host. Note that using this
|
||||
database hook implicitly enables both read_special and
|
||||
one_file_system (see above) to support dump and restore
|
||||
streaming.
|
||||
database hook implicitly enables read_special (see
|
||||
above) to support dump and restore streaming.
|
||||
example: users
|
||||
hostname:
|
||||
type: string
|
||||
@@ -2288,3 +2283,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)
|
||||
|
||||
@@ -34,7 +34,7 @@ def dump_data_sources(
|
||||
|
||||
Return an empty sequence, since there are no ongoing dump processes from this hook.
|
||||
'''
|
||||
if hook_config.get('store_config_files') is False:
|
||||
if hook_config and hook_config.get('store_config_files') is False:
|
||||
return []
|
||||
|
||||
borgmatic_manifest_path = os.path.join(
|
||||
@@ -0,0 +1,346 @@
|
||||
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
|
||||
|
||||
|
||||
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(' '))
|
||||
+ (
|
||||
'-t', # Filesystem type.
|
||||
'btrfs',
|
||||
'--json',
|
||||
'--list', # Request a flat list instead of a nested subvolume hierarchy.
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
return tuple(
|
||||
filesystem['target'] for filesystem in json.loads(findmnt_output)['filesystems']
|
||||
)
|
||||
except json.JSONDecodeError as error:
|
||||
raise ValueError(f'Invalid {findmnt_command} JSON output: {error}')
|
||||
except KeyError as error:
|
||||
raise ValueError(f'Invalid {findmnt_command} output: Missing key "{error}"')
|
||||
|
||||
|
||||
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 or Btrfs isn't configured in borgmatic's
|
||||
configuration, then don't actually remove anything.
|
||||
'''
|
||||
if hook_config is None:
|
||||
return
|
||||
|
||||
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):
|
||||
@@ -24,17 +16,19 @@ def make_data_source_dump_path(borgmatic_runtime_directory, data_source_hook_nam
|
||||
return os.path.join(borgmatic_runtime_directory, data_source_hook_name)
|
||||
|
||||
|
||||
def make_data_source_dump_filename(dump_path, name, hostname=None):
|
||||
def make_data_source_dump_filename(dump_path, name, hostname=None, port=None):
|
||||
'''
|
||||
Based on the given dump directory path, data source name, and hostname, return a filename to use
|
||||
for the data source dump. The hostname defaults to localhost.
|
||||
Based on the given dump directory path, data source name, hostname, and port, return a filename
|
||||
to use for the data source dump. The hostname defaults to localhost.
|
||||
|
||||
Raise ValueError if the data source name is invalid.
|
||||
'''
|
||||
if os.path.sep in name:
|
||||
raise ValueError(f'Invalid data source name {name}')
|
||||
|
||||
return os.path.join(dump_path, hostname or 'localhost', name)
|
||||
return os.path.join(
|
||||
dump_path, (hostname or 'localhost') + ('' if port is None else f':{port}'), name
|
||||
)
|
||||
|
||||
|
||||
def create_parent_directory_for_dump(dump_path):
|
||||
@@ -0,0 +1,405 @@
|
||||
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,
|
||||
'--permission',
|
||||
'r', # Read-only.
|
||||
'--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 or LVM isn't configured
|
||||
in borgmatic's configuration, then don't actually remove anything.
|
||||
'''
|
||||
if hook_config is None:
|
||||
return
|
||||
|
||||
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__)
|
||||
|
||||
@@ -73,7 +73,10 @@ def execute_dump_command(
|
||||
'''
|
||||
database_name = database['name']
|
||||
dump_filename = dump.make_data_source_dump_filename(
|
||||
dump_path, database['name'], database.get('hostname')
|
||||
dump_path,
|
||||
database['name'],
|
||||
database.get('hostname'),
|
||||
database.get('port'),
|
||||
)
|
||||
|
||||
if os.path.exists(dump_filename):
|
||||
@@ -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__)
|
||||
|
||||
@@ -51,7 +51,10 @@ def dump_data_sources(
|
||||
for database in databases:
|
||||
name = database['name']
|
||||
dump_filename = dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_runtime_directory), name, database.get('hostname')
|
||||
make_dump_path(borgmatic_runtime_directory),
|
||||
name,
|
||||
database.get('hostname'),
|
||||
database.get('port'),
|
||||
)
|
||||
dump_format = database.get('format', 'archive')
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -73,7 +73,10 @@ def execute_dump_command(
|
||||
'''
|
||||
database_name = database['name']
|
||||
dump_filename = dump.make_data_source_dump_filename(
|
||||
dump_path, database['name'], database.get('hostname')
|
||||
dump_path,
|
||||
database['name'],
|
||||
database.get('hostname'),
|
||||
database.get('port'),
|
||||
)
|
||||
|
||||
if os.path.exists(dump_filename):
|
||||
@@ -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__)
|
||||
|
||||
@@ -151,7 +151,10 @@ def dump_data_sources(
|
||||
for part in shlex.split(database.get('pg_dump_command') or default_dump_command)
|
||||
)
|
||||
dump_filename = dump.make_data_source_dump_filename(
|
||||
dump_path, database_name, database.get('hostname')
|
||||
dump_path,
|
||||
database_name,
|
||||
database.get('hostname'),
|
||||
database.get('port'),
|
||||
)
|
||||
if os.path.exists(dump_filename):
|
||||
logger.warning(
|
||||
@@ -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',
|
||||
@@ -233,16 +283,19 @@ def remove_data_source_dumps(hook_config, config, log_prefix, borgmatic_runtime_
|
||||
'''
|
||||
Given a ZFS configuration dict, a configuration dict, a log prefix, the borgmatic runtime
|
||||
directory, and whether this is a dry run, unmount and destroy any ZFS snapshots created by
|
||||
borgmatic. Use the log prefix in any log entries. If this is a dry run, then don't actually
|
||||
remove anything.
|
||||
borgmatic. Use the log prefix in any log entries. If this is a dry run or ZFS isn't configured
|
||||
in borgmatic's configuration, then don't actually remove anything.
|
||||
'''
|
||||
if hook_config is None:
|
||||
return
|
||||
|
||||
dry_run_label = ' (dry run; not actually removing anything)' if dry_run else ''
|
||||
|
||||
# Unmount snapshots.
|
||||
zfs_command = hook_config.get('zfs_command', 'zfs')
|
||||
|
||||
try:
|
||||
datasets = get_all_datasets(zfs_command)
|
||||
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 +318,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}'
|
||||
)
|
||||
+57
-54
@@ -1,75 +1,74 @@
|
||||
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 {}
|
||||
if hook_name in config or f'{hook_name}_databases' in config:
|
||||
hook_config = config.get(hook_name) or config.get(f'{hook_name}_databases') or {}
|
||||
else:
|
||||
hook_config = None
|
||||
|
||||
try:
|
||||
module = HOOK_NAME_TO_MODULE[hook_name]
|
||||
except KeyError:
|
||||
module_name = hook_name.split('_databases')[0]
|
||||
|
||||
# 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 +79,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__)
|
||||
|
||||
+32
-13
@@ -1,10 +1,9 @@
|
||||
import enum
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import sys
|
||||
|
||||
import colorama
|
||||
|
||||
|
||||
def to_bool(arg):
|
||||
'''
|
||||
@@ -33,7 +32,7 @@ def interactive_console():
|
||||
def should_do_markup(no_color, configs):
|
||||
'''
|
||||
Given the value of the command-line no-color argument, and a dict of configuration filename to
|
||||
corresponding parsed configuration, determine if we should enable colorama marking up.
|
||||
corresponding parsed configuration, determine if we should enable color marking up.
|
||||
'''
|
||||
if no_color:
|
||||
return False
|
||||
@@ -93,30 +92,50 @@ class Console_no_color_formatter(logging.Formatter):
|
||||
return record.msg
|
||||
|
||||
|
||||
class Color(enum.Enum):
|
||||
RESET = 0
|
||||
RED = 31
|
||||
GREEN = 32
|
||||
YELLOW = 33
|
||||
MAGENTA = 35
|
||||
CYAN = 36
|
||||
|
||||
|
||||
class Console_color_formatter(logging.Formatter):
|
||||
def format(self, record):
|
||||
add_custom_log_levels()
|
||||
|
||||
color = {
|
||||
logging.CRITICAL: colorama.Fore.RED,
|
||||
logging.ERROR: colorama.Fore.RED,
|
||||
logging.WARN: colorama.Fore.YELLOW,
|
||||
logging.ANSWER: colorama.Fore.MAGENTA,
|
||||
logging.INFO: colorama.Fore.GREEN,
|
||||
logging.DEBUG: colorama.Fore.CYAN,
|
||||
}.get(record.levelno)
|
||||
color = (
|
||||
{
|
||||
logging.CRITICAL: Color.RED,
|
||||
logging.ERROR: Color.RED,
|
||||
logging.WARN: Color.YELLOW,
|
||||
logging.ANSWER: Color.MAGENTA,
|
||||
logging.INFO: Color.GREEN,
|
||||
logging.DEBUG: Color.CYAN,
|
||||
}
|
||||
.get(record.levelno)
|
||||
.value
|
||||
)
|
||||
|
||||
return color_text(color, record.msg)
|
||||
|
||||
|
||||
def ansi_escape_code(color): # pragma: no cover
|
||||
'''
|
||||
Given a color value, produce the corresponding ANSI escape code.
|
||||
'''
|
||||
return f'\x1b[{color}m'
|
||||
|
||||
|
||||
def color_text(color, message):
|
||||
'''
|
||||
Give colored text.
|
||||
Given a color value and a message, return the message colored with ANSI escape codes.
|
||||
'''
|
||||
if not color:
|
||||
return message
|
||||
|
||||
return f'{color}{message}{colorama.Style.RESET_ALL}'
|
||||
return f'{ansi_escape_code(color)}{message}{ansi_escape_code(Color.RESET.value)}'
|
||||
|
||||
|
||||
def add_logging_level(level_name, level_number):
|
||||
|
||||
@@ -391,7 +391,7 @@ with the repository's path or its label as configured in your borgmatic
|
||||
configuration file.
|
||||
|
||||
```bash
|
||||
borgmatic restore --repository repo.borg --archive host-2023-...
|
||||
borgmatic restore --repository repo.borg --archive latest
|
||||
```
|
||||
|
||||
### Restore particular databases
|
||||
@@ -401,7 +401,7 @@ restore one of them, use the `--database` flag to select one or more
|
||||
databases. For instance:
|
||||
|
||||
```bash
|
||||
borgmatic restore --archive host-2023-... --database users --database orders
|
||||
borgmatic restore --archive latest --database users --database orders
|
||||
```
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.7.6</span> You can
|
||||
@@ -409,29 +409,99 @@ also restore individual databases even if you dumped them as "all"—as long as
|
||||
you dumped them into separate files via use of the "format" option. See above
|
||||
for more information.
|
||||
|
||||
### Restore databases sharing a name
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.9.5</span> If you've
|
||||
backed up multiple databases that happen to share the same name but different
|
||||
hostnames, ports, or hooks, you can include additional flags to disambiguate
|
||||
which database you'd like to restore. For instance, let's say you've backed up
|
||||
the following configured databases:
|
||||
|
||||
```yaml
|
||||
postgresql_databases:
|
||||
- name: users
|
||||
hostname: host1.example.org
|
||||
- name: users
|
||||
hostname: host2.example.org
|
||||
```
|
||||
|
||||
... then you can run the following command to restore only one of them:
|
||||
|
||||
```bash
|
||||
borgmatic restore --archive latest --database users --original-hostname host1.example.org
|
||||
```
|
||||
|
||||
This selects a `users` database to restore, but only if it originally came
|
||||
from the host `host1.example.org`. This command won't restore `users`
|
||||
databases from any other hosts.
|
||||
|
||||
Here's another example configuration:
|
||||
|
||||
```yaml
|
||||
postgresql_databases:
|
||||
- name: users
|
||||
hostname: example.org
|
||||
port: 5433
|
||||
- name: users
|
||||
hostname: example.org
|
||||
port: 5434
|
||||
```
|
||||
|
||||
And a command to restore just one of the databases:
|
||||
|
||||
```bash
|
||||
borgmatic restore --archive latest --database users --original-port 5433
|
||||
```
|
||||
|
||||
That restores a `users` database only if it originally came from port `5433`
|
||||
*and* if that port is in borgmatic's configuration, e.g. `port: 5433`.
|
||||
|
||||
Finally, check out this configuration:
|
||||
|
||||
```yaml
|
||||
postgresql_databases:
|
||||
- name: users
|
||||
hostname: example.org
|
||||
mariadb_databases:
|
||||
- name: users
|
||||
hostname: example.org
|
||||
```
|
||||
|
||||
And to select just one of the databases to restore:
|
||||
|
||||
```bash
|
||||
borgmatic restore --archive latest --database users --hook postgresql
|
||||
```
|
||||
|
||||
That restores a `users` database only if it was dumped using the
|
||||
`postgresql_databases:` data source hook. This command won't restore `users`
|
||||
databases that were dumped using other hooks.
|
||||
|
||||
Note that these flags don't change the hostname or port to which the database
|
||||
is actually restored. For that, see below about restoring to an alternate
|
||||
host.
|
||||
|
||||
|
||||
### Restore all databases
|
||||
|
||||
To restore all databases:
|
||||
|
||||
```bash
|
||||
borgmatic restore --archive host-2023-... --database all
|
||||
borgmatic restore --archive latest --database all
|
||||
```
|
||||
|
||||
Or omit the `--database` flag entirely:
|
||||
|
||||
|
||||
```bash
|
||||
borgmatic restore --archive host-2023-...
|
||||
borgmatic restore --archive latest
|
||||
```
|
||||
|
||||
Prior to borgmatic version 1.7.6, this restores a combined "all" database
|
||||
dump from the archive.
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.7.6</span> Restoring
|
||||
"all" databases restores each database found in the selected archive. That
|
||||
includes any combined dump file named "all" and any other individual database
|
||||
dumps found in the archive.
|
||||
dumps found in the archive. Prior to borgmatic version 1.7.6, restoring "all"
|
||||
only restored a combined "all" database dump from the archive.
|
||||
|
||||
|
||||
### Restore particular schemas
|
||||
@@ -466,21 +536,35 @@ postgresql_databases:
|
||||
restore_password: trustsome1
|
||||
```
|
||||
|
||||
### Manual restoration
|
||||
|
||||
### Limitations
|
||||
If you prefer to restore a database without the help of borgmatic, first
|
||||
[extract](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/) an
|
||||
archive containing a database dump.
|
||||
|
||||
borgmatic extracts the dump file into the `borgmatic/` directory within the
|
||||
extraction destination path. For example, if you're extracting to `/tmp`, then
|
||||
the dump will be in `/tmp/borgmatic/`.
|
||||
|
||||
<span class="minilink minilink-addedin">Prior to version 1.9.0</span> borgmatic
|
||||
extracts the dump file into the *`username`*`/.borgmatic/` directory within the
|
||||
extraction destination path, where *`username`* is the user that created the
|
||||
backup. For example, if you created the backup with the `root` user and you're
|
||||
extracting to `/tmp`, then the dump will be in `/tmp/root/.borgmatic`.
|
||||
|
||||
After extraction, you can manually restore the dump file using native database
|
||||
commands like `pg_restore`, `mysql`, `mongorestore`, `sqlite`, or similar.
|
||||
|
||||
Also see the documentation on [listing database
|
||||
dumps](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/#listing-database-dumps).
|
||||
|
||||
|
||||
## Limitations
|
||||
|
||||
There are a few important limitations with borgmatic's current database
|
||||
restoration feature that you should know about:
|
||||
hooks that you should know about:
|
||||
|
||||
1. You must restore as the same Unix user that created the archive containing
|
||||
the database dump. That's because the user's home directory path is encoded
|
||||
into the path of the database dump within the archive.
|
||||
2. As mentioned above, borgmatic can only restore a database that's defined in
|
||||
borgmatic's own configuration file. So include your configuration file in
|
||||
backups to avoid getting caught without a way to restore a database.
|
||||
3. borgmatic does not currently support backing up or restoring multiple
|
||||
databases that share the exact same name on different hosts.
|
||||
4. When database hooks are enabled, borgmatic instructs Borg to consume
|
||||
1. When database hooks are enabled, borgmatic instructs Borg to consume
|
||||
special files (via `--read-special`) to support database dump
|
||||
streaming—regardless of the value of your `read_special` configuration option.
|
||||
And because this can cause Borg to hang, borgmatic also automatically excludes
|
||||
@@ -495,7 +579,10 @@ exclude them. <span class="minilink minilink-addedin">Prior to version
|
||||
1.7.3</span>Special files were not auto-excluded, and you were responsible for
|
||||
excluding them yourself. Common directories to exclude are `/dev` and `/run`,
|
||||
but that may not be exhaustive.
|
||||
5. <span class="minilink minilink-addedin">Prior to version 1.9.0</span>
|
||||
2. <span class="minilink minilink-addedin">Prior to version 1.9.5</span>
|
||||
borgmatic did not support backing up or restoring multiple databases that
|
||||
shared the exact same name on different hosts or with different ports.
|
||||
3. <span class="minilink minilink-addedin">Prior to version 1.9.0</span>
|
||||
Database hooks also implicitly enabled the `one_file_system` option, which
|
||||
meant Borg wouldn't cross filesystem boundaries when looking for files to
|
||||
backup. When borgmatic was running in a container, this often required a
|
||||
@@ -503,25 +590,16 @@ work-around to explicitly add each mounted backup volume to
|
||||
`source_directories` instead of relying on Borg to include them implicitly via
|
||||
a parent directory. But as of borgmatic 1.9.0, `one_file_system` is no longer
|
||||
auto-enabled and such work-arounds aren't necessary.
|
||||
|
||||
|
||||
### Manual restoration
|
||||
|
||||
If you prefer to restore a database without the help of borgmatic, first
|
||||
[extract](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/) an
|
||||
archive containing a database dump.
|
||||
|
||||
borgmatic extracts the dump file into the *`username`*`/.borgmatic/` directory
|
||||
within the extraction destination path, where *`username`* is the user that
|
||||
created the backup. For example, if you created the backup with the `root`
|
||||
user and you're extracting to `/tmp`, then the dump will be in
|
||||
`/tmp/root/.borgmatic`.
|
||||
|
||||
After extraction, you can manually restore the dump file using native database
|
||||
commands like `pg_restore`, `mysql`, `mongorestore`, `sqlite`, or similar.
|
||||
|
||||
Also see the documentation on [listing database
|
||||
dumps](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/#listing-database-dumps).
|
||||
4. <span class="minilink minilink-addedin">Prior to version 1.9.0</span> You
|
||||
must restore as the same Unix user that created the archive containing the
|
||||
database dump. That's because the user's home directory path is encoded into
|
||||
the path of the database dump within the archive.
|
||||
5. <span class="minilink minilink-addedin">Prior to version 1.7.15</span> As
|
||||
mentioned above, borgmatic can only restore a database that's defined in
|
||||
borgmatic's own configuration file. So include your configuration files in
|
||||
backups to avoid getting caught without a way to restore a database. But
|
||||
starting from version 1.7.15, borgmatic includes your configuration files
|
||||
automatically.
|
||||
|
||||
|
||||
## Preparation and cleanup hooks
|
||||
|
||||
@@ -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), temporarily 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,166 @@ 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 the 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, temporarily
|
||||
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 |
+5
-6
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "borgmatic"
|
||||
version = "1.9.3"
|
||||
version = "1.9.5"
|
||||
authors = [
|
||||
{ name="Dan Helfman", email="witten@torsion.org" },
|
||||
]
|
||||
@@ -17,11 +17,10 @@ classifiers=[
|
||||
"Topic :: System :: Archiving :: Backup",
|
||||
]
|
||||
dependencies = [
|
||||
"colorama>=0.4.1,<0.5",
|
||||
"jsonschema",
|
||||
"packaging",
|
||||
"requests",
|
||||
"ruamel.yaml>0.15.0",
|
||||
"jsonschema",
|
||||
"packaging",
|
||||
"requests",
|
||||
"ruamel.yaml>0.15.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -6,7 +6,6 @@ certifi==2024.7.4
|
||||
chardet==5.2.0
|
||||
click==8.1.7
|
||||
codespell==2.2.6
|
||||
colorama==0.4.6
|
||||
coverage==7.5.1
|
||||
flake8==7.0.0
|
||||
flake8-quotes==3.4.0
|
||||
|
||||
@@ -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,42 @@
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def parse_arguments(*unparsed_arguments):
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
parser.add_argument('-t', dest='type')
|
||||
parser.add_argument('--json', action='store_true')
|
||||
parser.add_argument('--list', action='store_true')
|
||||
|
||||
return parser.parse_args(unparsed_arguments)
|
||||
|
||||
|
||||
BUILTIN_FILESYSTEM_MOUNT_OUTPUT = '''{
|
||||
"filesystems": [
|
||||
{
|
||||
"target": "/mnt/subvolume",
|
||||
"source": "/dev/loop0",
|
||||
"fstype": "btrfs",
|
||||
"options": "rw,relatime,ssd,space_cache=v2,subvolid=5,subvol=/"
|
||||
}
|
||||
]
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
def print_filesystem_mounts():
|
||||
print(BUILTIN_FILESYSTEM_MOUNT_OUTPUT)
|
||||
|
||||
|
||||
def main():
|
||||
arguments = parse_arguments(*sys.argv[1:])
|
||||
|
||||
assert arguments.type == 'btrfs'
|
||||
assert arguments.json
|
||||
assert arguments.list
|
||||
|
||||
print_filesystem_mounts()
|
||||
|
||||
|
||||
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,44 @@
|
||||
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('--permission', required=True)
|
||||
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)
|
||||
@@ -16,6 +16,8 @@ def write_configuration(
|
||||
repository_path,
|
||||
user_runtime_directory,
|
||||
postgresql_dump_format='custom',
|
||||
postgresql_all_dump_format=None,
|
||||
mariadb_mysql_all_dump_format=None,
|
||||
mongodb_dump_format='archive',
|
||||
):
|
||||
'''
|
||||
@@ -23,6 +25,13 @@ def write_configuration(
|
||||
for testing. This includes injecting the given repository path, borgmatic source directory for
|
||||
storing database dumps, dump format (for PostgreSQL), and encryption passphrase.
|
||||
'''
|
||||
postgresql_all_format_option = (
|
||||
f'format: {postgresql_all_dump_format}' if postgresql_all_dump_format else ''
|
||||
)
|
||||
mariadb_mysql_dump_format_option = (
|
||||
f'format: {mariadb_mysql_all_dump_format}' if mariadb_mysql_all_dump_format else ''
|
||||
)
|
||||
|
||||
config_yaml = f'''
|
||||
source_directories:
|
||||
- {source_directory}
|
||||
@@ -39,11 +48,7 @@ postgresql_databases:
|
||||
password: test
|
||||
format: {postgresql_dump_format}
|
||||
- name: all
|
||||
hostname: postgresql
|
||||
username: postgres
|
||||
password: test
|
||||
- name: all
|
||||
format: custom
|
||||
{postgresql_all_format_option}
|
||||
hostname: postgresql
|
||||
username: postgres
|
||||
password: test
|
||||
@@ -53,11 +58,7 @@ mariadb_databases:
|
||||
username: root
|
||||
password: test
|
||||
- name: all
|
||||
hostname: mariadb
|
||||
username: root
|
||||
password: test
|
||||
- name: all
|
||||
format: sql
|
||||
{mariadb_mysql_dump_format_option}
|
||||
hostname: mariadb
|
||||
username: root
|
||||
password: test
|
||||
@@ -67,11 +68,7 @@ mysql_databases:
|
||||
username: root
|
||||
password: test
|
||||
- name: all
|
||||
hostname: not-actually-mysql
|
||||
username: root
|
||||
password: test
|
||||
- name: all
|
||||
format: sql
|
||||
{mariadb_mysql_dump_format_option}
|
||||
hostname: not-actually-mysql
|
||||
username: root
|
||||
password: test
|
||||
@@ -97,12 +94,21 @@ sqlite_databases:
|
||||
return ruamel.yaml.YAML(typ='safe').load(config_yaml)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'postgresql_all_dump_format,mariadb_mysql_all_dump_format',
|
||||
(
|
||||
(None, None),
|
||||
('custom', 'sql'),
|
||||
),
|
||||
)
|
||||
def write_custom_restore_configuration(
|
||||
source_directory,
|
||||
config_path,
|
||||
repository_path,
|
||||
user_runtime_directory,
|
||||
postgresql_dump_format='custom',
|
||||
postgresql_all_dump_format=None,
|
||||
mariadb_mysql_all_dump_format=None,
|
||||
mongodb_dump_format='archive',
|
||||
):
|
||||
'''
|
||||
|
||||
@@ -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():
|
||||
+634
-221
File diff suppressed because it is too large
Load Diff
@@ -275,7 +275,8 @@ def test_collect_special_file_paths_parses_special_files_from_borg_dry_run_file_
|
||||
'Processing files ...\n- /foo\n+ /bar\n- /baz'
|
||||
)
|
||||
flexmock(module).should_receive('special_file').and_return(True)
|
||||
flexmock(module).should_receive('any_parent_directories').and_return(False)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module).should_receive('any_parent_directories').never()
|
||||
|
||||
assert module.collect_special_file_paths(
|
||||
('borg', 'create'),
|
||||
@@ -283,17 +284,24 @@ def test_collect_special_file_paths_parses_special_files_from_borg_dry_run_file_
|
||||
local_path=None,
|
||||
working_directory=None,
|
||||
borg_environment=None,
|
||||
skip_directories=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
) == ('/foo', '/bar', '/baz')
|
||||
|
||||
|
||||
def test_collect_special_file_paths_excludes_requested_directories():
|
||||
def test_collect_special_file_paths_skips_borgmatic_runtime_directory():
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
|
||||
'+ /foo\n- /bar\n- /baz'
|
||||
'+ /foo\n- /run/borgmatic/bar\n- /baz'
|
||||
)
|
||||
flexmock(module).should_receive('special_file').and_return(True)
|
||||
flexmock(module).should_receive('any_parent_directories').and_return(False).and_return(
|
||||
True
|
||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||
flexmock(module).should_receive('any_parent_directories').with_args(
|
||||
'/foo', ('/run/borgmatic',)
|
||||
).and_return(False)
|
||||
flexmock(module).should_receive('any_parent_directories').with_args(
|
||||
'/run/borgmatic/bar', ('/run/borgmatic',)
|
||||
).and_return(True)
|
||||
flexmock(module).should_receive('any_parent_directories').with_args(
|
||||
'/baz', ('/run/borgmatic',)
|
||||
).and_return(False)
|
||||
|
||||
assert module.collect_special_file_paths(
|
||||
@@ -302,10 +310,29 @@ def test_collect_special_file_paths_excludes_requested_directories():
|
||||
local_path=None,
|
||||
working_directory=None,
|
||||
borg_environment=None,
|
||||
skip_directories=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
) == ('/foo', '/baz')
|
||||
|
||||
|
||||
def test_collect_special_file_paths_with_borgmatic_runtime_directory_missing_from_paths_output_errors():
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
|
||||
'+ /foo\n- /bar\n- /baz'
|
||||
)
|
||||
flexmock(module).should_receive('special_file').and_return(True)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||
flexmock(module).should_receive('any_parent_directories').and_return(False)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.collect_special_file_paths(
|
||||
('borg', 'create'),
|
||||
config={},
|
||||
local_path=None,
|
||||
working_directory=None,
|
||||
borg_environment=None,
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
def test_collect_special_file_paths_excludes_non_special_files():
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
|
||||
'+ /foo\n+ /bar\n+ /baz'
|
||||
@@ -313,7 +340,8 @@ def test_collect_special_file_paths_excludes_non_special_files():
|
||||
flexmock(module).should_receive('special_file').and_return(True).and_return(False).and_return(
|
||||
True
|
||||
)
|
||||
flexmock(module).should_receive('any_parent_directories').and_return(False)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module).should_receive('any_parent_directories').never()
|
||||
|
||||
assert module.collect_special_file_paths(
|
||||
('borg', 'create'),
|
||||
@@ -321,7 +349,7 @@ def test_collect_special_file_paths_excludes_non_special_files():
|
||||
local_path=None,
|
||||
working_directory=None,
|
||||
borg_environment=None,
|
||||
skip_directories=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
) == ('/foo', '/baz')
|
||||
|
||||
|
||||
@@ -335,7 +363,8 @@ def test_collect_special_file_paths_omits_exclude_no_dump_flag_from_command():
|
||||
borg_exit_codes=None,
|
||||
).and_return('Processing files ...\n- /foo\n+ /bar\n- /baz').once()
|
||||
flexmock(module).should_receive('special_file').and_return(True)
|
||||
flexmock(module).should_receive('any_parent_directories').and_return(False)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module).should_receive('any_parent_directories').never()
|
||||
|
||||
module.collect_special_file_paths(
|
||||
('borg', 'create', '--exclude-nodump'),
|
||||
@@ -343,7 +372,7 @@ def test_collect_special_file_paths_omits_exclude_no_dump_flag_from_command():
|
||||
local_path='borg',
|
||||
working_directory=None,
|
||||
borg_environment=None,
|
||||
skip_directories=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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():
|
||||
@@ -22,7 +22,7 @@ def test_dump_data_sources_creates_manifest_file():
|
||||
).once()
|
||||
|
||||
module.dump_data_sources(
|
||||
hook_config={},
|
||||
hook_config=None,
|
||||
config={},
|
||||
log_prefix='test',
|
||||
config_paths=('test.yaml',),
|
||||
@@ -53,7 +53,7 @@ def test_dump_data_sources_with_dry_run_does_not_create_manifest_file():
|
||||
flexmock(module.json).should_receive('dump').never()
|
||||
|
||||
module.dump_data_sources(
|
||||
hook_config={},
|
||||
hook_config=None,
|
||||
config={},
|
||||
log_prefix='test',
|
||||
config_paths=('test.yaml',),
|
||||
@@ -74,7 +74,7 @@ def test_remove_data_source_dumps_deletes_manifest_and_parent_directory():
|
||||
flexmock(module.os).should_receive('rmdir').with_args('/run/borgmatic/bootstrap').once()
|
||||
|
||||
module.remove_data_source_dumps(
|
||||
hook_config={},
|
||||
hook_config=None,
|
||||
config={},
|
||||
log_prefix='test',
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
@@ -91,7 +91,7 @@ def test_remove_data_source_dumps_with_dry_run_bails():
|
||||
flexmock(module.os).should_receive('rmdir').never()
|
||||
|
||||
module.remove_data_source_dumps(
|
||||
hook_config={},
|
||||
hook_config=None,
|
||||
config={},
|
||||
log_prefix='test',
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
@@ -110,7 +110,7 @@ def test_remove_data_source_dumps_swallows_manifest_file_not_found_error():
|
||||
flexmock(module.os).should_receive('rmdir').with_args('/run/borgmatic/bootstrap').once()
|
||||
|
||||
module.remove_data_source_dumps(
|
||||
hook_config={},
|
||||
hook_config=None,
|
||||
config={},
|
||||
log_prefix='test',
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
@@ -131,7 +131,7 @@ def test_remove_data_source_dumps_swallows_manifest_parent_directory_not_found_e
|
||||
).once()
|
||||
|
||||
module.remove_data_source_dumps(
|
||||
hook_config={},
|
||||
hook_config=None,
|
||||
config={},
|
||||
log_prefix='test',
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
@@ -0,0 +1,800 @@
|
||||
import pytest
|
||||
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(
|
||||
'''{
|
||||
"filesystems": [
|
||||
{
|
||||
"target": "/mnt0",
|
||||
"source": "/dev/loop0",
|
||||
"fstype": "btrfs",
|
||||
"options": "rw,relatime,ssd,space_cache=v2,subvolid=5,subvol=/"
|
||||
},
|
||||
{
|
||||
"target": "/mnt1",
|
||||
"source": "/dev/loop0",
|
||||
"fstype": "btrfs",
|
||||
"options": "rw,relatime,ssd,space_cache=v2,subvolid=5,subvol=/"
|
||||
}
|
||||
]
|
||||
}
|
||||
'''
|
||||
)
|
||||
|
||||
assert module.get_filesystem_mount_points('findmnt') == ('/mnt0', '/mnt1')
|
||||
|
||||
|
||||
def test_get_filesystem_mount_points_with_invalid_findmnt_json_errors():
|
||||
flexmock(module.borgmatic.execute).should_receive(
|
||||
'execute_command_and_capture_output'
|
||||
).and_return('{')
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.get_filesystem_mount_points('findmnt')
|
||||
|
||||
|
||||
def test_get_filesystem_mount_points_with_findmnt_json_missing_filesystems_errors():
|
||||
flexmock(module.borgmatic.execute).should_receive(
|
||||
'execute_command_and_capture_output'
|
||||
).and_return('{"wtf": "something is wrong here"}')
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.get_filesystem_mount_points('findmnt')
|
||||
|
||||
|
||||
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_without_hook_configuration_bails():
|
||||
flexmock(module).should_receive('get_subvolumes').never()
|
||||
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=None,
|
||||
config={'source_directories': '/mnt/subvolume'},
|
||||
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():
|
||||
@@ -15,6 +15,13 @@ def test_make_data_source_dump_filename_uses_name_and_hostname():
|
||||
)
|
||||
|
||||
|
||||
def test_make_data_source_dump_filename_uses_name_and_hostname_and_port():
|
||||
assert (
|
||||
module.make_data_source_dump_filename('databases', 'test', 'hostname', 1234)
|
||||
== 'databases/hostname:1234/test'
|
||||
)
|
||||
|
||||
|
||||
def test_make_data_source_dump_filename_without_hostname_defaults_to_localhost():
|
||||
assert module.make_data_source_dump_filename('databases', 'test') == 'databases/localhost/test'
|
||||
|
||||
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')
|
||||
@@ -217,8 +321,23 @@ def test_remove_data_source_dumps_use_custom_commands():
|
||||
)
|
||||
|
||||
|
||||
def test_remove_data_source_dumps_bails_for_missing_hook_configuration():
|
||||
flexmock(module).should_receive('get_all_dataset_mount_points').never()
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'replace_temporary_subdirectory_with_glob'
|
||||
).never()
|
||||
|
||||
module.remove_data_source_dumps(
|
||||
hook_config=None,
|
||||
config={'source_directories': '/mnt/dataset'},
|
||||
log_prefix='test',
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_remove_data_source_dumps_bails_for_missing_zfs_command():
|
||||
flexmock(module).should_receive('get_all_datasets').and_raise(FileNotFoundError)
|
||||
flexmock(module).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 +353,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 +371,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 +395,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 +419,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 +444,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 +473,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,13 +35,67 @@ 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
|
||||
None, 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)
|
||||
@@ -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
|
||||
|
||||
@@ -187,10 +187,14 @@ def test_console_color_formatter_format_includes_log_message():
|
||||
|
||||
|
||||
def test_color_text_does_not_raise():
|
||||
module.color_text(module.colorama.Fore.RED, 'hi')
|
||||
flexmock(module).should_receive('ansi_escape_code').and_return('blah')
|
||||
|
||||
module.color_text(module.Color.RED, 'hi')
|
||||
|
||||
|
||||
def test_color_text_without_color_does_not_raise():
|
||||
flexmock(module).should_receive('ansi_escape_code').and_return('blah')
|
||||
|
||||
module.color_text(None, 'hi')
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user