Compare commits

...
33 Commits
Author SHA1 Message Date
Dan Helfman 08ff743cd7 Bump version for release. 2025-12-20 17:16:51 -08:00
Dan Helfman 406b533b15 Adjust Btrfs snapshot paths so that Borg 1.x gets file cache hits when backing them up (#1206).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/1206
2025-12-21 01:10:33 +00:00
Dan Helfman 18a1d4f00b Update documention mentioning Btrfs snapshot path containing process ID (#1206). 2025-12-20 17:01:06 -08:00
Dan Helfman 54d955bc95 Fix automated tests and update NEWS (#1206). 2025-12-20 16:59:13 -08:00
Dan Helfman a72f9b24b5 Merge branch jacksonchen666-btrfs-remove-pid into btrfs-remove-pid and streamline the docs a bit. 2025-12-20 16:02:42 -08:00
Dan Helfman 1b471f5346 Code formatting (#1054). 2025-12-19 23:20:38 -08:00
Dan Helfman 9428a92297 Fix broken Btrfs end-to-end test (#1054). 2025-12-19 23:10:33 -08:00
Dan Helfman c0d4489c25 Add unnecessary fake Btrfs read-only flag in end-to-end test (#1054). 2025-12-19 19:15:09 -08:00
Dan Helfman b4ea45ab74 Allow the Btrfs hook to create and delete snapshots even when running as a non-root user (#1054). 2025-12-19 19:10:31 -08:00
Dan Helfman 6ae9b973fc For the MariaDB and MySQL database hooks, creates a consistent snapshot by dumping all tables in a single transaction (#1193). 2025-12-19 11:53:30 -08:00
Dan Helfman 01c7d0a0db Fix that errors and exits when the borgmatic runtime directory is partially excluded by configured excludes (#1203). 2025-12-18 19:04:42 -08:00
Dan Helfman a31f913f54 For the MariaDB and MySQL database hooks, add a "socket_path" option for Unix socket database connections (#1193). 2025-12-14 21:13:21 -08:00
Dan Helfman 0b086a3c18 Fix documentation typo (#1201). 2025-12-13 10:02:41 -08:00
Dan Helfman dfdb1cb15e Document a problematic interaction between borgmatic and systemd-tmpfiles (#1201). 2025-12-13 09:57:33 -08:00
Dan Helfman c8307065fa Add a "file_list_format" option for setting the "list" action's output format and an "archive_list_format" option for setting the "repo-list" action's format (#1179). 2025-12-12 22:31:41 -08:00
Dan Helfman ae00770c35 Upgrade urllib3 in test requirements for security fixes. 2025-12-09 11:01:18 -08:00
Dan Helfman 77782bc215 "Fix" code coverage complaint. 2025-12-09 10:59:30 -08:00
Dan Helfman ff2009fe19 Fix ruff errors by removing unused imports (#1200).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/1200
2025-12-09 18:57:58 +00:00
Christian Henriksen 141f2d68c1 Fix ruff errors by removing unused imports. 2025-12-09 11:47:47 +01:00
Dan Helfman 772c4c1b83 In the documentation for the MariaDB/MySQL database hooks, clarify how to set custom command-line flags for database commands (#1193). 2025-12-03 13:52:30 -08:00
Dan Helfman a77de447d4 Document potential interactions between systemd security settings and the ZFS, LVM, and Btrfs hooks (#1192, #1163). 2025-12-02 17:25:36 -08:00
Dan Helfman 50a062611e Fix a regression in the ZFS, LVM, and Btrfs hooks in which snapshotted paths ignored global excludes (#1195). 2025-12-02 15:40:42 -08:00
Dan Helfman 42bf6c6f8e Fix typo in database restore documentation example (#1197). 2025-12-02 12:13:06 -08:00
Dan Helfman e95d652abb Reword NEWS entry for clarity (#1192). 2025-12-02 10:58:29 -08:00
Dan Helfman 2a97ac0174 Fix for over-aggressive deduplication of source directories that contain the borgmatic runtime directory (#1192). 2025-12-01 17:14:31 -08:00
Dan Helfman 5029ab1ffd Update the sample systemd timer with a shorter random delay when catching up on a missed run. 2025-11-29 22:25:43 -08:00
Dan Helfman 7dc902f5db Fix for an incorrect diff command shown when running the "generate config" action with a source configuration file (#1194). 2025-11-28 19:53:46 -08:00
Dan Helfman 8134bdec7f Fix release script brokenness. 2025-11-26 15:40:16 -08:00
JacksonandDan Helfman f158d42b28 apply docs suggestions
Co-authored-by: Dan Helfman <witten@torsion.org>
2025-11-23 09:20:03 +01:00
Jackson dec47298bd somehow make a test work 2025-11-22 08:03:40 +01:00
Jackson 4976352fd3 add to btrfs docs that caching works now 2025-11-22 05:49:15 +01:00
Jackson f95fbaa6aa fix at least some tests (with changed snapshot path)
this really is just a blanket find and replace though
2025-11-21 09:22:34 +01:00
Jackson 7dc7b77f6c static btrfs snapshot paths
i don't know the implications of this change, except for maybe
concurrent borgmatic processes
2025-11-21 08:07:33 +01:00
51 changed files with 930 additions and 583 deletions
+32
View File
@@ -1,3 +1,35 @@
2.0.13
* #1054: Allow the Btrfs hook to create and delete snapshots even when running
as a non-root user. See the documentation for more information:
https://torsion.org/borgmatic/reference/configuration/data-sources/btrfs/#non-root-user
* #1122: To prevent the user from inadvertently excluding the "bootstrap" action's manifest, always
error and exit when the borgmatic runtime directory overlaps with the configured excludes.
* #1179: Add a "file_list_format" option for setting the "list" action's output format and an
"archive_list_format" option for setting the "repo-list" action's format.
* #1192: Fix for over-aggressive deduplication of source directories that contain the borgmatic
runtime directory, potentially resulting in data loss (data not getting backed up) when
snapshotting these source directories.
* #1192, #1163: Document potential interactions between security settings in borgmatic's sample
systemd service file and the ZFS, LVM, and Btrfs hooks.
* #1193: In the documentation for the MariaDB/MySQL database hooks, clarify how to set custom
command-line flags for database commands.
* #1193: For the MariaDB and MySQL database hooks, add a "socket_path" option for Unix socket
database connections.
* #1193: For the MariaDB and MySQL database hooks, creates a consistent snapshot by dumping all
tables in a single transaction.
* #1194: Fix for an incorrect diff command shown when running the "generate config" action with a
source configuration file.
* #1195: Fix a regression in the ZFS, LVM, and Btrfs hooks in which snapshotted paths ignored
global excludes.
* #1201: Document a problematic interaction between borgmatic and systemd-tmpfiles:
https://torsion.org/borgmatic/reference/configuration/runtime-directory/#systemd-tmpfiles
* #1203: Fix that errors and exits when the borgmatic runtime directory is partially excluded by
configured excludes. Previously, borgmatic only errored when the runtime directory was completely
excluded.
* #1206: Adjust Btrfs snapshot paths so that Borg 1.x gets file cache hits when backing them up,
improving performance.
* Update the sample systemd timer with a shorter random delay when catching up on a missed run.
2.0.12
* #1127: Fix for the database hooks not respecting the "working_directory" option.
* #1181: Add an "ask_for_password" option to the KeePassXC credential hook for disabling
-2
View File
@@ -20,10 +20,8 @@ import borgmatic.borg.extract
import borgmatic.borg.list
import borgmatic.borg.pattern
import borgmatic.borg.repo_list
import borgmatic.borg.state
import borgmatic.config.paths
import borgmatic.execute
import borgmatic.hooks.command
DEFAULT_CHECKS = (
{'name': 'repository', 'frequency': '1 month'},
-1
View File
@@ -2,7 +2,6 @@ import logging
import borgmatic.borg.compact
import borgmatic.borg.feature
import borgmatic.hooks.command
logger = logging.getLogger(__name__)
-1
View File
@@ -5,7 +5,6 @@ import os
import borgmatic.borg.extract
import borgmatic.borg.repo_list
import borgmatic.config.paths
import borgmatic.hooks.command
logger = logging.getLogger(__name__)
+2 -2
View File
@@ -36,12 +36,12 @@ def run_generate(generate_arguments, global_arguments):
if generate_arguments.source_filename:
logger.answer(
f'''
Merged in the contents of configuration file at: {generate_arguments.source_filename}'''
Merged the contents of the source configuration file: {generate_arguments.source_filename}'''
)
if not generate_arguments.split:
logger.answer(
'''To review the changes made, run:
f'''To review the changes made, run:
diff --unified {generate_arguments.source_filename} {generate_arguments.destination_path}''',
)
+1
View File
@@ -49,6 +49,7 @@ def run_create(
working_directory,
borgmatic_runtime_directory,
)
original_patterns = list(patterns)
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
'remove_data_source_dumps',
-1
View File
@@ -2,7 +2,6 @@ import logging
import borgmatic.actions.arguments
import borgmatic.borg.delete
import borgmatic.borg.repo_delete
import borgmatic.borg.repo_list
logger = logging.getLogger(__name__)
-1
View File
@@ -2,7 +2,6 @@ import logging
import borgmatic.borg.extract
import borgmatic.borg.repo_list
import borgmatic.hooks.command
logger = logging.getLogger(__name__)
+20 -14
View File
@@ -259,31 +259,37 @@ def deduplicate_runtime_directory_patterns(patterns, config, borgmatic_runtime_d
return patterns
deduplicated = {} # Use just the keys as an ordered set.
runtime_directory_parents = set(pathlib.PurePath(borgmatic_runtime_directory).parents).union(
{pathlib.PurePath(borgmatic_runtime_directory)}
)
for pattern in patterns:
if pattern.type != borgmatic.borg.pattern.Pattern_type.ROOT:
deduplicated[pattern] = True
continue
parents = pathlib.PurePath(pattern.path).parents
pattern_parents = pathlib.PurePath(pattern.path).parents
# If another directory in the given list is a parent of current directory (even n levels up)
# and both are on the same filesystem (or one_file_system is not set), then the current
# directory is a duplicate.
# If:
#
# 1. another pattern is a parent of the current pattern (even n levels up),
# 2. both patterns are parents of the runtime directory (even n levels up),
# 3. and both patterns are on the same filesystem (or one_file_system is not set)
#
# ... then consider the current pattern as a duplicate.
for other_pattern in patterns:
if other_pattern.type != borgmatic.borg.pattern.Pattern_type.ROOT:
continue
if any(
pathlib.PurePath(other_pattern.path) == parent
and pathlib.PurePosixPath(other_pattern.path)
in pathlib.PurePath(borgmatic_runtime_directory).parents
and pattern.device is not None
and (
other_pattern.device == pattern.device
or config.get('one_file_system') is not True
)
for parent in parents
device_matches = pattern.device is not None and (
other_pattern.device == pattern.device or config.get('one_file_system') is not True
)
if (
pathlib.PurePath(other_pattern.path) in pattern_parents
and pathlib.PurePosixPath(other_pattern.path) in runtime_directory_parents
and pathlib.PurePosixPath(pattern.path) in runtime_directory_parents
and device_matches
):
break
else:
-1
View File
@@ -1,7 +1,6 @@
import logging
import borgmatic.borg.prune
import borgmatic.hooks.command
logger = logging.getLogger(__name__)
-1
View File
@@ -8,7 +8,6 @@ import tempfile
import borgmatic.actions.pattern
import borgmatic.borg.extract
import borgmatic.borg.list
import borgmatic.borg.mount
import borgmatic.borg.repo_list
import borgmatic.config.paths
import borgmatic.hooks.data_source.dump
+16 -19
View File
@@ -96,32 +96,29 @@ def validate_planned_backup_paths(
if path_line and path_line.startswith(('- ', '+ '))
)
# These are the subset of output paths contained within the borgmatic runtime directory.
paths_inside_runtime_directory = {
path for path in paths if any_parent_directories(path, (borgmatic_runtime_directory,))
}
# If the runtime directory isn't present in the source patterns, then we shouldn't expect it to
# be in the paths output from the Borg dry run.
runtime_directory_present_in_patterns = any(
runtime_directory_root_patterns = tuple(
pattern
for pattern in patterns
if any_parent_directories(pattern.path, (borgmatic_runtime_directory,))
if pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT
)
# If no paths to backup are inside the runtime directory, it must've been excluded.
if (
not paths_inside_runtime_directory
and runtime_directory_present_in_patterns
and not dry_run
and os.path.exists(borgmatic_runtime_directory)
):
raise ValueError(
f'The runtime directory {os.path.normpath(borgmatic_runtime_directory)} overlaps with the configured excludes or patterns with excludes. Please ensure the runtime directory is not excluded.',
)
if not dry_run and os.path.exists(borgmatic_runtime_directory):
# If there are any root patterns in the runtime directory that are missing from the paths
# Borg is planning to backup, then they must've gotten excluded, e.g. by user-configured
# excludes. Error accordingly.
for pattern in runtime_directory_root_patterns:
if not any(any_parent_directories(path, (pattern.path,)) for path in paths):
raise ValueError(
f'The runtime directory {os.path.normpath(borgmatic_runtime_directory)} overlaps with the configured excludes or patterns with excludes. Please ensure the runtime directory is not excluded.',
)
return tuple(path for path in paths if path not in paths_inside_runtime_directory)
# Return the subset of output paths *not* contained within the borgmatic runtime directory. The
# intent is that any downstream checks using these paths should skip runtime paths that
# borgmatic uses for its own bookkeeping, instead focusing on user-configured paths.
return tuple(
path for path in paths if not any_parent_directories(path, (borgmatic_runtime_directory,))
)
MAX_SPECIAL_FILE_PATHS_LENGTH = 1000
-1
View File
@@ -3,7 +3,6 @@ import os
import shlex
import borgmatic.config.paths
import borgmatic.logger
from borgmatic.borg import environment, flags
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
+2
View File
@@ -18,6 +18,7 @@ MAKE_FLAGS_EXCLUDES = (
'archive',
'paths',
'find_paths',
'format',
*ARCHIVE_FILTER_FLAGS_MOVED_TO_REPO_LIST,
)
@@ -54,6 +55,7 @@ def make_list_command(
+ flags.make_flags('umask', config.get('umask'))
+ flags.make_flags('log-json', config.get('log_json'))
+ flags.make_flags('lock-wait', config.get('lock_wait'))
+ flags.make_flags('format', list_arguments.format or config.get('file_list_format'))
+ flags.make_flags_from_arguments(list_arguments, excludes=MAKE_FLAGS_EXCLUDES)
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
+ (
+4 -1
View File
@@ -110,7 +110,7 @@ def get_latest_archive(
return latest_archive
MAKE_FLAGS_EXCLUDES = ('repository', 'prefix', 'match_archives')
MAKE_FLAGS_EXCLUDES = ('repository', 'format', 'prefix', 'match_archives')
def make_repo_list_command(
@@ -170,6 +170,9 @@ def make_repo_list_command(
)
)
)
+ flags.make_flags(
'format', repo_list_arguments.format or config.get('archive_list_format')
)
+ flags.make_flags_from_arguments(repo_list_arguments, excludes=MAKE_FLAGS_EXCLUDES)
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
+ flags.make_repository_flags(repository_path, local_borg_version)
+1 -1
View File
@@ -1 +1 @@
DEFAULT_BORGMATIC_SOURCE_DIRECTORY = '~/.borgmatic'
DEFAULT_BORGMATIC_SOURCE_DIRECTORY = '~/.borgmatic' # pragma: nocover
+10 -3
View File
@@ -297,7 +297,14 @@ def parse_arguments_for_actions(unparsed_arguments, action_parsers, global_parse
)
OMITTED_FLAG_NAMES = {'match-archives', 'progress', 'statistics', 'list-details'}
OMITTED_FLAG_NAMES = {
'match-archives',
'progress',
'statistics',
'list-details',
'file-list-format',
'archive-list-format',
}
def make_argument_description(schema, flag_name):
@@ -1541,7 +1548,7 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
action='store_true',
help='Output only archive names',
)
repo_list_group.add_argument('--format', help='Format for archive listing')
repo_list_group.add_argument('--format', help='Borg format for the archive listing')
repo_list_group.add_argument(
'--json',
default=False,
@@ -1644,7 +1651,7 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
action='store_true',
help='Output only path names',
)
list_group.add_argument('--format', help='Format for file listing')
list_group.add_argument('--format', help='Borg format for the file listing')
list_group.add_argument(
'--json',
default=False,
+76 -16
View File
@@ -177,7 +177,9 @@ properties:
as "source_directories"; they tell Borg which paths to backup
(modulo any excludes). Globs are expanded. (Tildes are not.) See
the output of "borg help patterns" for more details. Quote any value
if it contains leading punctuation, so it parses correctly.
if it contains leading punctuation, so it parses correctly. Also use
leading slashes in absolute paths, or data source hooks may be
unable to rewrite patterns as needed.
example:
- 'R /'
- '- /home/*/.cache'
@@ -492,6 +494,27 @@ properties:
If match_archives is not specified, borgmatic defaults to deriving
the match_archives value from archive_name_format.
example: "sh:{hostname}-*"
file_list_format:
type: string
description: |
Borg format for the files listing of the "list" action. Corresponds
to the "--format" flag. Defaults to "{mode} {user:6} {group:6}
{size:8} {mtime} {path}{extra}{NL}". With "--json", the form of the
format is ignored, but the keys used in it are added to the JSON
output. See
https://borgbackup.readthedocs.io/en/stable/usage/list.html for
details.
example: "{path}{extra}{NL}"
archive_list_format:
type: string
description: |
Borg format for the archives listing of the "repo-list" action.
Corresponds to the "--format" flag. Defaults to "{archive:<36}
{time} [{id}]{NL}". With "--json", the form of the format is
ignored, but the keys used in it are added to the JSON output. See
https://borgbackup.readthedocs.io/en/stable/usage/list.html for
details.
example: "{archive}{NL}"
relocated_repo_access_is_ok:
type: boolean
description: |
@@ -998,6 +1021,8 @@ properties:
type: string
description: |
Python format string used for log messages written to the log file.
See https://docs.python.org/3/library/logging.html (and specifically
the LogRecord attributes with "{}-formatting") for details.
example: "[{asctime}] {levelname}: {prefix}{message}"
monitoring_verbosity:
type: integer
@@ -1694,6 +1719,20 @@ properties:
description: |
Port to restore to. Defaults to the "port" option.
example: 5433
socket_path:
type: string
description: |
Path of a Unix socket to connect to instead of a remote
hostname. Ignored when "hostname" is set and not
"localhost". Defaults to "/run/mysqld/mysqld.sock".
example: database.example.org
restore_socket_path:
type: string
description: |
Path of a Unix socket to connect to instead of a remote
hostname. Ignored when "hostname" is set and not
"localhost". Defaults to the "socket_path" option.
example: database.example.org
username:
type: string
description: |
@@ -1757,18 +1796,21 @@ properties:
mariadb_dump_command:
type: string
description: |
Command to use instead of "mariadb-dump". This can be
used to run a specific mariadb_dump version (e.g., one
inside a running container). If you run it from within a
container, make sure to mount the path in the
"user_runtime_directory" option from the host into the
container at the same location. Defaults to
"mariadb-dump".
Command to use instead of "mariadb-dump". But set any
additional command-line flags in "options", not here.
This command can be used to run a specific mariadb_dump
version (e.g., one inside a running container). If you
run it from within a container, make sure to mount the
path in the "user_runtime_directory" option from the
host into the container at the same location. Defaults
to "mariadb-dump".
example: docker exec mariadb_container mariadb-dump
mariadb_command:
type: string
description: |
Command to run instead of "mariadb". This can be used to
Command to run instead of "mariadb". But set any
additional command-line flags in "list_options" or
"restore_options", not here. This command can be used to
run a specific mariadb version (e.g., one inside a
running container). Defaults to "mariadb".
example: docker exec mariadb_container mariadb
@@ -1885,6 +1927,20 @@ properties:
description: |
Port to restore to. Defaults to the "port" option.
example: 5433
socket_path:
type: string
description: |
Path of a Unix socket to connect to instead of a remote
hostname. Ignored when "hostname" is set and not
"localhost". Defaults to "/run/mysqld/mysqld.sock".
example: database.example.org
restore_socket_path:
type: string
description: |
Path of a Unix socket to connect to instead of a remote
hostname. Ignored when "hostname" is set and not
"localhost". Defaults to the "socket_path" option.
example: database.example.org
username:
type: string
description: |
@@ -1948,17 +2004,21 @@ properties:
mysql_dump_command:
type: string
description: |
Command to use instead of "mysqldump". This can be used
to run a specific mysql_dump version (e.g., one inside a
running container). If you run it from within a
container, make sure to mount the path in the
"user_runtime_directory" option from the host into the
container at the same location. Defaults to "mysqldump".
Command to use instead of "mysqldump". But set any
additional command-line flags in "options", not here.
This command can be used to run a specific mysql_dump
version (e.g., one inside a running container). If you
run it from within a container, make sure to mount the
path in the "user_runtime_directory" option from the
host into the container at the same location. Defaults
to "mysqldump".
example: docker exec mysql_container mysqldump
mysql_command:
type: string
description: |
Command to run instead of "mysql". This can be used to
Command to run instead of "mysql". But set any
additional command-line flags in "list_options" or
"restore_options", not here. This command can be used to
run a specific mysql version (e.g., one inside a running
container). Defaults to "mysql".
example: docker exec mysql_container mysql
+19 -35
View File
@@ -23,33 +23,24 @@ def use_streaming(hook_config, config): # pragma: no cover
return False
BTRFS_SUBVOLUME_INODE_NUMBER = 256
@functools.cache
def path_is_a_subvolume(btrfs_command, path):
def path_is_a_subvolume(path):
'''
Given a btrfs command and a path, return whether the path is a Btrfs subvolume. Return False if
the btrfs command errors, which probably indicates there isn't a containing Btrfs subvolume for
the given path.
Given a path, return whether it is a Btrfs subvolume. Return False if the path doesn't exist.
As a performance optimization, multiple calls to this function with the same arguments are
cached.
As a performance optimization, multiple calls to this function with the same path are cached.
'''
if path == os.environ.get('BTRFS_TEST_SUBVOLUME_PATH'): # pragma: no cover
return True
try:
borgmatic.execute.execute_command(
(
*btrfs_command.split(' '),
'subvolume',
'show',
path,
),
output_log_level=None,
close_fds=True,
)
# An error from the command (probably) indicates that the path is not actually a subvolume.
except subprocess.CalledProcessError:
return os.stat(path).st_ino == BTRFS_SUBVOLUME_INODE_NUMBER
except FileNotFoundError:
return False
return True
@functools.cache
def get_subvolume_property(btrfs_command, subvolume_path, property_name):
@@ -99,7 +90,7 @@ def get_containing_subvolume_path(btrfs_command, path):
path,
*tuple(str(ancestor) for ancestor in pathlib.PurePath(path).parents),
):
if not path_is_a_subvolume(btrfs_command, candidate_path):
if not path_is_a_subvolume(candidate_path):
continue
try:
@@ -180,7 +171,7 @@ def get_subvolumes(btrfs_command, patterns):
return tuple(sorted(subvolumes, key=lambda subvolume: subvolume.path))
BORGMATIC_SNAPSHOT_PREFIX = '.borgmatic-snapshot-'
BORGMATIC_SNAPSHOT_PREFIX = '.borgmatic-snapshot'
def make_snapshot_path(subvolume_path):
@@ -189,7 +180,7 @@ def make_snapshot_path(subvolume_path):
'''
return os.path.join(
subvolume_path,
f'{BORGMATIC_SNAPSHOT_PREFIX}{os.getpid()}',
f'{BORGMATIC_SNAPSHOT_PREFIX}',
# Included so that the snapshot ends up in the Borg archive at the "original" subvolume path.
) + subvolume_path.rstrip(os.path.sep)
@@ -202,16 +193,16 @@ def make_snapshot_exclude_pattern(subvolume_path): # pragma: no cover
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
/mnt/.borgmatic-snapshot/mnt
... then the snapshot itself will have an empty directory at:
/mnt/.borgmatic-snapshot-1234/mnt/.borgmatic-snapshot-1234
/mnt/.borgmatic-snapshot/mnt/.borgmatic-snapshot
So to prevent that from ending up in the Borg archive, this function produces an exclude pattern
to exclude that path.
'''
snapshot_directory = f'{BORGMATIC_SNAPSHOT_PREFIX}{os.getpid()}'
snapshot_directory = f'{BORGMATIC_SNAPSHOT_PREFIX}'
return borgmatic.borg.pattern.Pattern(
os.path.join(
@@ -244,7 +235,7 @@ def make_borg_snapshot_pattern(subvolume_path, pattern):
rewritten_path = initial_caret + os.path.join(
subvolume_path,
f'{BORGMATIC_SNAPSHOT_PREFIX}{os.getpid()}',
f'{BORGMATIC_SNAPSHOT_PREFIX}',
# Use the Borg 1.4+ "slashdot" hack to prevent the snapshot path prefix from getting
# included in the archive—but only if there's not already a slashdot hack present in the
# pattern.
@@ -274,7 +265,6 @@ def snapshot_subvolume(btrfs_command, subvolume_path, snapshot_path): # pragma:
*btrfs_command.split(' '),
'subvolume',
'snapshot',
'-r', # Read-only.
subvolume_path,
snapshot_path,
),
@@ -329,15 +319,9 @@ def dump_data_sources(
snapshot_subvolume(btrfs_command, subvolume.path, snapshot_path)
last_contained_pattern_index = borgmatic.hooks.data_source.config.get_last_pattern_index(
patterns, subvolume.contained_patterns
)
for pattern in subvolume.contained_patterns:
snapshot_pattern = make_borg_snapshot_pattern(subvolume.path, pattern)
borgmatic.hooks.data_source.config.replace_pattern(
patterns, pattern, snapshot_pattern, last_contained_pattern_index
)
borgmatic.hooks.data_source.config.replace_pattern(patterns, pattern, snapshot_pattern)
borgmatic.hooks.data_source.config.inject_pattern(
patterns, make_snapshot_exclude_pattern(subvolume.path)
+9 -64
View File
@@ -1,4 +1,3 @@
import contextlib
import json
import logging
import shutil
@@ -106,18 +105,20 @@ def get_ip_from_container(container):
)
def inject_pattern(patterns, data_source_pattern):
def inject_pattern(patterns, data_source_pattern, override_excludes=True):
'''
Given a list of borgmatic.borg.pattern.Pattern instances representing the configured patterns,
insert the given data source pattern at the start of the list. The idea is that borgmatic is
injecting its own custom pattern specific to a data source hook into the user's configured
patterns so that the hook's data gets included in the backup.
As part of this injection, if the data source pattern is a root pattern, also insert an
"include" version of the given root pattern, in an attempt to preempt any of the user's
configured exclude patterns that may follow.
As part of this injection, if the data source pattern is a root pattern and override_excludes is
True, also insert an "include" version of the given root pattern, in an attempt to preempt any
of the user's configured exclude patterns that may follow. The is to support use cases like
borgmatic injecting its own patterns for things like database dumps or bootstrap metadata, where
we don't want them to get accidentally excluded.
'''
if data_source_pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT:
if data_source_pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT and override_excludes:
patterns.insert(
0,
borgmatic.borg.pattern.Pattern(
@@ -132,77 +133,21 @@ def inject_pattern(patterns, data_source_pattern):
patterns.insert(0, data_source_pattern)
def get_last_pattern_index(patterns, patterns_subset):
'''
Given a sequence of all patterns and a subset of those patterns, find each subset pattern in the
all patterns sequence and return the highest (last) index.
'''
last_pattern_index = 0
for pattern in patterns_subset:
with contextlib.suppress(ValueError):
last_pattern_index = max(patterns.index(pattern), last_pattern_index)
return last_pattern_index
def replace_pattern(
patterns, pattern_to_replace, data_source_pattern, last_contained_pattern_index
):
def replace_pattern(patterns, pattern_to_replace, data_source_pattern):
'''
Given a list of borgmatic.borg.pattern.Pattern instances representing the configured patterns,
replace the given pattern with the given data source pattern. The idea is that borgmatic is
replacing a configured pattern with its own modified pattern specific to a data source hook so
that the hook's data gets included in the backup.
As part of this replacement, if the data source pattern is a root pattern, also insert an
"include" version of the given root pattern right after the given last contained pattern index
in an attempt to preempt any of the user's configured global exclude patterns that may follow.
But we don't want to preempt any intentional partial excludes of the data source pattern itself,
which is why the include goes after the last contained pattern index.
For instance, let's say that the patterns are effectively:
R /foo
R /bar
- /bar/.cache
R /baz
- **
... and "R /bar" is the pattern to replace, data source pattern is "R /bar/snapshot", and the
last contained pattern index is 2 (corresponding to "- /bar/.cache"). The resulting patterns
after calling this function would be:
R /foo
R /bar/snapshot
- /bar/snapshot/.cache
+ /bar/snapshot
R /baz
- **
Note that the positioning of "+ /bar/snapshot" means that it overrides the "- **" global exclude
but not the "- /bar/snapshot/.cache" contained pattern exclude.
If the pattern to replace can't be found in the given patterns, then just inject the data source
pattern at the start of the list.
'''
try:
index = patterns.index(pattern_to_replace)
except ValueError:
inject_pattern(patterns, data_source_pattern)
inject_pattern(patterns, data_source_pattern, override_excludes=False)
return
patterns[index] = data_source_pattern
if data_source_pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT:
patterns.insert(
last_contained_pattern_index + 1,
borgmatic.borg.pattern.Pattern(
path=data_source_pattern.path,
type=borgmatic.borg.pattern.Pattern_type.INCLUDE,
style=data_source_pattern.style,
device=data_source_pattern.device,
source=borgmatic.borg.pattern.Pattern_source.HOOK,
),
)
+1 -7
View File
@@ -269,10 +269,6 @@ def dump_data_sources(
snapshot_mount_path,
)
last_contained_pattern_index = borgmatic.hooks.data_source.config.get_last_pattern_index(
patterns, logical_volume.contained_patterns
)
for pattern in logical_volume.contained_patterns:
snapshot_pattern = make_borg_snapshot_pattern(
pattern,
@@ -280,9 +276,7 @@ def dump_data_sources(
normalized_runtime_directory,
)
borgmatic.hooks.data_source.config.replace_pattern(
patterns, pattern, snapshot_pattern, last_contained_pattern_index
)
borgmatic.hooks.data_source.config.replace_pattern(patterns, pattern, snapshot_pattern)
return []
+10
View File
@@ -132,6 +132,8 @@ def database_names_to_dump(database, config, username, password, environment, dr
extra_options, defaults_extra_filename = parse_extra_options(database.get('list_options'))
password_transport = database.get('password_transport', 'pipe')
hostname = database_config.resolve_database_option('hostname', database)
socket_path = database.get('socket_path')
show_command = (
mariadb_show_command
+ (
@@ -143,6 +145,7 @@ def database_names_to_dump(database, config, username, password, environment, dr
+ (('--host', hostname) if hostname else ())
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if hostname or 'port' in database else ())
+ (('--socket', socket_path) if socket_path else ())
+ (('--user', username) if username and password_transport == 'environment' else ())
+ (('--ssl',) if database.get('tls') is True else ())
+ (('--skip-ssl',) if database.get('tls') is False else ())
@@ -213,6 +216,8 @@ def execute_dump_command(
extra_options, defaults_extra_filename = parse_extra_options(database.get('options'))
password_transport = database.get('password_transport', 'pipe')
hostname = database_config.resolve_database_option('hostname', database)
socket_path = database.get('socket_path')
dump_command = (
mariadb_dump_command
+ (
@@ -222,9 +227,11 @@ def execute_dump_command(
)
+ extra_options
+ (('--add-drop-database',) if database.get('add_drop_database', True) else ())
+ ('--single-transaction',)
+ (('--host', hostname) if hostname else ())
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if hostname or 'port' in database else ())
+ (('--socket', socket_path) if socket_path else ())
+ (('--user', username) if username and password_transport == 'environment' else ())
+ (('--ssl',) if database.get('tls') is True else ())
+ (('--skip-ssl',) if database.get('tls') is False else ())
@@ -448,6 +455,7 @@ def restore_data_source_dump(
port = database_config.resolve_database_option(
'port', data_source, connection_params, restore=True
)
socket_path = database_config.resolve_database_option('socket_path', data_source, restore=True)
tls = database_config.resolve_database_option('tls', data_source, restore=True)
username = borgmatic.hooks.credential.parse.resolve_credential(
database_config.resolve_database_option(
@@ -467,6 +475,7 @@ def restore_data_source_dump(
)
extra_options, defaults_extra_filename = parse_extra_options(data_source.get('restore_options'))
password_transport = data_source.get('password_transport', 'pipe')
restore_command = (
mariadb_restore_command
+ (
@@ -479,6 +488,7 @@ def restore_data_source_dump(
+ (('--host', hostname) if hostname else ())
+ (('--port', str(port)) if port else ())
+ (('--protocol', 'tcp') if hostname or port else ())
+ (('--socket', socket_path) if socket_path else ())
+ (('--user', username) if username and password_transport == 'environment' else ())
+ (('--ssl',) if tls is True else ())
+ (('--skip-ssl',) if tls is False else ())
+10
View File
@@ -57,6 +57,8 @@ def database_names_to_dump(database, config, username, password, environment, dr
)
password_transport = database.get('password_transport', 'pipe')
hostname = database_config.resolve_database_option('hostname', database)
socket_path = database.get('socket_path')
show_command = (
mysql_show_command
+ (
@@ -72,6 +74,7 @@ def database_names_to_dump(database, config, username, password, environment, dr
+ (('--host', hostname) if hostname else ())
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if hostname or 'port' in database else ())
+ (('--socket', socket_path) if socket_path else ())
+ (('--user', username) if username and password_transport == 'environment' else ())
+ (('--ssl',) if database.get('tls') is True else ())
+ (('--skip-ssl',) if database.get('tls') is False else ())
@@ -140,6 +143,8 @@ def execute_dump_command(
)
password_transport = database.get('password_transport', 'pipe')
hostname = database_config.resolve_database_option('hostname', database)
socket_path = database.get('socket_path')
dump_command = (
mysql_dump_command
+ (
@@ -153,9 +158,11 @@ def execute_dump_command(
)
+ extra_options
+ (('--add-drop-database',) if database.get('add_drop_database', True) else ())
+ ('--single-transaction',)
+ (('--host', hostname) if hostname else ())
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if hostname or 'port' in database else ())
+ (('--socket', socket_path) if socket_path else ())
+ (('--user', username) if username and password_transport == 'environment' else ())
+ (('--ssl',) if database.get('tls') is True else ())
+ (('--skip-ssl',) if database.get('tls') is False else ())
@@ -379,6 +386,7 @@ def restore_data_source_dump(
port = database_config.resolve_database_option(
'port', data_source, connection_params, restore=True
)
socket_path = database_config.resolve_database_option('socket_path', data_source, restore=True)
tls = database_config.resolve_database_option('tls', data_source, restore=True)
username = borgmatic.hooks.credential.parse.resolve_credential(
database_config.resolve_database_option(
@@ -400,6 +408,7 @@ def restore_data_source_dump(
borgmatic.hooks.data_source.mariadb.parse_extra_options(data_source.get('restore_options'))
)
password_transport = data_source.get('password_transport', 'pipe')
restore_command = (
mysql_restore_command
+ (
@@ -416,6 +425,7 @@ def restore_data_source_dump(
+ (('--host', hostname) if hostname else ())
+ (('--port', str(port)) if port else ())
+ (('--protocol', 'tcp') if hostname or port else ())
+ (('--socket', socket_path) if socket_path else ())
+ (('--user', username) if username and password_transport == 'environment' else ())
+ (('--ssl',) if tls is True else ())
+ (('--skip-ssl',) if tls is False else ())
+1 -7
View File
@@ -300,10 +300,6 @@ def dump_data_sources(
snapshot_mount_path,
)
last_contained_pattern_index = borgmatic.hooks.data_source.config.get_last_pattern_index(
patterns, dataset.contained_patterns
)
for pattern in dataset.contained_patterns:
snapshot_pattern = make_borg_snapshot_pattern(
pattern,
@@ -311,9 +307,7 @@ def dump_data_sources(
normalized_runtime_directory,
)
borgmatic.hooks.data_source.config.replace_pattern(
patterns, pattern, snapshot_pattern, last_contained_pattern_index
)
borgmatic.hooks.data_source.config.replace_pattern(patterns, pattern, snapshot_pattern)
return []
-1
View File
@@ -3,7 +3,6 @@ import importlib
import logging
import pkgutil
import borgmatic.hooks.command
import borgmatic.hooks.credential
import borgmatic.hooks.data_source
import borgmatic.hooks.monitoring
+12 -2
View File
@@ -249,6 +249,10 @@ container separate from borgmatic.
A similar approach can work with MySQL, using `mysql_dump_command` instead of
`mariadb_dump_command` to run `mysqldump` in a container.
Note: With MariaDB and MySQL, use `options`, `list_options`, and/or
`restore_options` to override command-line flags rather than putting flags into
`mariadb_dump_command`, `mysql_dump_command`, etc.
#### Database client in a temporary container
@@ -544,12 +548,13 @@ Or from the configuration file:
postgresql_databases:
- name: users
hostname: database1.example.org
restore_hostname: database1.example.org
restore_hostname: database2.example.org
restore_port: 5433
restore_username: postgres
restore_password: trustsome1
```
### Manual restoration
If you prefer to restore a database without the help of borgmatic, first
@@ -654,9 +659,14 @@ configuration. Here's an example with MariaDB:
```yaml
mariadb_databases:
- name: posts
options: "--single-transaction --quick"
options: "--single-transaction"
```
<span class="minilink minilink-addedin">New in version 2.0.13</span> borgmatic
passes `--single-transaction` to MariaDB/MySQL by default, and you no longer
need to set that in `options:`.
### borgmatic hangs during backup
See Limitations above about `read_special`. You may need to exclude certain
@@ -60,7 +60,7 @@ 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 path is `/var/subvolume`, then the snapshotted files will appear in an
archive at `/var/subvolume` as well—even if borgmatic has to mount the snapshot
somewhere in `/var/subvolume/.borgmatic-snapshot-1234/` to perform the backup.
somewhere in `/var/subvolume/.borgmatic-snapshot/` 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
@@ -70,26 +70,46 @@ temporary snapshot directory in use at the time the archive was created, as Borg
## Performance
<span class="minilink minilink-addedin">With Borg version 1.x</span> Because of
the way that Btrfs snapshot paths change from one borgmatic invocation to the
next, the [Borg file
<span class="minilink minilink-addedin">New in borgmatic version 2.0.13, with
Borg version 1.x</span> borgmatic uses consistent snapshot paths between
invocations, so Btrfs snapshots are cached correctly. No configuration is
necessary.
<span class="minilink minilink-addedin">Prior to borgmatic version 2.0.13, with
Borg version 1.x</span> Because of the way that Btrfs snapshot paths change from
one borgmatic invocation to the next, the [Borg file
cache](https://borgbackup.readthedocs.io/en/stable/internals/data-structures.html#cache)
will never get cache hits on snapshotted files. This makes backing up Btrfs
never gets cache hits on snapshotted files. This makes backing up Btrfs
snapshots a little slower than non-snapshotted files that have consistent paths.
**It is also not possible to mitigate cache misses**, as the Btrfs hook uses
snapshot paths which change between borgmatic invocations, and the snapshots
are located outside the [runtime
directory](https://torsion.org/borgmatic/reference/configuration/runtime-directory/),
contrary to
[ZFS](https://torsion.org/borgmatic/reference/configuration/data-sources/zfs/#performance)
and
[LVM](https://torsion.org/borgmatic/reference/configuration/data-sources/lvm/#performance).
If this is an issue for you, upgrade to borgmatic to 2.0.13+.
<span class="minilink minilink-addedin">With Borg version 2.x</span> Even
snapshotted files should get cache hits, because Borg 2.x is smarter about how
it looks up file paths in its cache—it constructs the cache key with the path
*as it's seen in the archive* (which is consistent across runs) rather than the
full absolute source path (which changes).
snapshotted files get cache hits, because Borg 2.x is smarter about how it looks
up file paths in its cache—it constructs the cache key with the path *as it's
seen in the archive* (which is consistent across runs) rather than the full
absolute source path (which changes).
## systemd settings
If you're using [systemd to run
borgmatic](https://torsion.org/borgmatic/how-to/set-up-backups/#systemd), you
may need to disable particular security settings like `CapabilityBoundingSet` to
allow the Btrfs feature to work. See the comments in the sample systemd service
file for details.
## non-root user
<span class="minilink minilink-addedin">New in version 2.0.13</span> If you'd
like borgmatic to snapshot a Btrfs subvolume when running as a non-root user,
make the following changes to the subvolume in question:
1. `chown` the subvolume to be owned by your non-root user.
2. Mount the subvolume with the `user_subvol_rm_allowed` mount option.
These changes allow the non-root user to create and delete snapshots of the
subvolume, which is necessary for the borgmatic Btrfs hook to work.
## Full configuration
@@ -114,6 +114,15 @@ the cache key with the path *as it's seen in the archive* (which is consistent
across runs) rather than the full absolute source path (which can change).
## systemd settings
If you're using [systemd to run
borgmatic](https://torsion.org/borgmatic/how-to/set-up-backups/#systemd), you
may need to disable particular security settings like `ProtectKernelModules`,
`CapabilityBoundingSet`, and/or `PrivateDevices` to allow the LVM feature to
work. See the comments in the sample systemd service file for details.
## Full configuration
```yaml
@@ -103,6 +103,15 @@ the cache key with the path *as it's seen in the archive* (which is consistent
across runs) rather than the full absolute source path (which can change).
## systemd settings
If you're using [systemd to run
borgmatic](https://torsion.org/borgmatic/how-to/set-up-backups/#systemd), you
may need to disable particular security settings like `CapabilityBoundingSet`
and/or `PrivateDevices` to allow the ZFS feature to work. See the comments in
the sample systemd service file for details.
## Full configuration
```yaml
@@ -39,3 +39,28 @@ system temporary directories.
borgmatic created temporary streaming database dumps within the `~/.borgmatic`
directory by default. At that time, the path was configurable by the
`borgmatic_source_directory` configuration option (now deprecated).
## systemd-tmpfiles
If borgmatic's runtime directory is in `/tmp`, be aware that some systems may
automatically delete `/tmp` files on a periodic basis, e.g. via
[systemd-tmpfiles](https://www.freedesktop.org/software/systemd/man/251/systemd-tmpfiles.html).
One sign that this is happening is borgmatic erroring during cleanup with "No
such file or directory" on the runtime directory path, indicating that
borgmatic's runtime directory is getting deleted out from under it.
You can work around this by either excluding borgmatic's runtime directory from
automatic systemd-tmpfiles management—or you can change borgmatic's runtime
directory to not be in `/tmp` as described above.
Here's what a systemd-tmpfiles exclude for borgmatic might look like, for
instance in an `/etc/tmpfiles.d/borgmatic.conf` file:
```
x /tmp/borgmatic-*
```
That tells systemd-tmpfiles to ignore borgmatic's runtime directory when
automatically deleting paths in `/tmp`.
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "borgmatic"
version = "2.0.12"
version = "2.0.13"
authors = [
{ name="Dan Helfman", email="witten@torsion.org" },
]
+7 -2
View File
@@ -26,13 +26,16 @@ LockPersonality=true
# But you can try setting it to "yes" for improved security if you don't use those features.
MemoryDenyWriteExecute=no
NoNewPrivileges=yes
# Filesystem hooks like ZFS may not work unless PrivateDevices is disabled.
# Filesystem hooks like ZFS and LVM may not work unless PrivateDevices is disabled.
PrivateDevices=yes
PrivateTmp=yes
ProtectClock=yes
ProtectControlGroups=yes
ProtectHostname=yes
ProtectKernelLogs=yes
# You may need to disable this if you're using the LVM hook. But an alternative
# might be to leave this enabled and preload the necessary kernel modules like
# "dm_snapshot" at boot, so this service doesn't need to load them on demand.
ProtectKernelModules=yes
ProtectKernelTunables=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
@@ -54,7 +57,9 @@ ProtectSystem=full
# BindReadOnlyPaths=-/root/.ssh
# May interfere with running external programs within borgmatic hooks. This
# includes, for instance, programs to snapshot filesystems (e.g. ZFS).
# includes, for instance, programs to snapshot filesystems (e.g. ZFS, LVM, and
# Btrfs). But rather than commenting this out entirely, one workaround may be to
# add "CAP_SYS_ADMIN".
CapabilityBoundingSet=CAP_DAC_READ_SEARCH CAP_NET_RAW
# Lower CPU and I/O priority.
+1 -1
View File
@@ -4,7 +4,7 @@ Description=Run borgmatic backup
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=3h
RandomizedDelaySec=10m
[Install]
WantedBy=timers.target
+1
View File
@@ -17,4 +17,5 @@ rm borgmatic-docs-dump.tar
mv html borgmatic-docs
rm -f dist/borgmatic-docs.tar.gz
tar cfz dist/borgmatic-docs.tar.gz borgmatic-docs
rm -fr borgmatic-docs
podman rm --volumes "$docs_container_id"
+2 -2
View File
@@ -34,7 +34,7 @@ git push github $version
# Build borgmatic and publish to pypi.
rm -fr dist
uv build
tarball_path=$(ls dist/borgmatic-*.tar.gz)
tarball_path="dist/borgmatic-$version.tar.gz"
wheel_path=$(ls dist/borgmatic-*-py3-none-any.whl)
twine upload -r pypi --username __token__ "$tarball_path"
twine upload -r pypi --username __token__ "$wheel_path"
@@ -51,7 +51,7 @@ release_id=$(curl --silent --request POST \
--header "Authorization: token $projects_token" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--data "{\"body\": \"$escaped_release_changelog\", \"draft\": false, \"name\": \"borgmatic $version\", \"prerelease\": false, \"tag_name\": \"$version\"}"
--data "{\"body\": \"$escaped_release_changelog\", \"draft\": false, \"name\": \"borgmatic $version\", \"prerelease\": false, \"tag_name\": \"$version\"}" \
| jq ".id")
curl --silent --request POST \
"https://projects.torsion.org/api/v1/repos/borgmatic-collective/borgmatic/releases/$release_id/assets?name=$(basename $wheel_path)" \
+1 -1
View File
@@ -24,4 +24,4 @@ requests-oauthlib==2.0.0
rpds-py==0.28.0
ruamel-yaml>0.15.0
typing-extensions==4.15.0
urllib3==2.5.0
urllib3==2.6.1
-1
View File
@@ -16,7 +16,6 @@ def parse_arguments(*unparsed_arguments):
show_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')
@@ -18,6 +18,7 @@ def generate_configuration(config_path, repository_path):
.replace('ssh://user@backupserver/./sourcehostname.borg', repository_path)
.replace('- path: /e2e/mnt/backup', '')
.replace('label: local', '')
.replace('- /home/user/path with spaces', '')
.replace('- /home', f'- {config_path}')
.replace('- /etc', '- /e2e/mnt/subvolume/subdir')
.replace('- /var/log/syslog*', '')
@@ -43,7 +44,10 @@ def test_btrfs_create_and_list():
)
# Run a create action to exercise Btrfs snapshotting and backup.
subprocess.check_call(f'borgmatic --config {config_path} create'.split(' '))
subprocess.check_call(
f'borgmatic -v 2 --config {config_path} create'.split(' '),
env=dict(os.environ, BTRFS_TEST_SUBVOLUME_PATH='/e2e/mnt/subvolume'),
)
# List the resulting archive and assert that the snapshotted files are there.
output = subprocess.check_output(
@@ -24,16 +24,15 @@ def test_dump_data_sources_snapshots_each_subvolume_and_updates_patterns():
module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
),
)
flexmock(module.os).should_receive('getpid').and_return(1234)
flexmock(module).should_receive('snapshot_subvolume').with_args(
'btrfs',
'/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
).once()
flexmock(module).should_receive('snapshot_subvolume').with_args(
'btrfs',
'/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/mnt/subvol2',
).once()
assert (
@@ -50,21 +49,19 @@ def test_dump_data_sources_snapshots_each_subvolume_and_updates_patterns():
assert patterns == [
Pattern(
'/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2/.borgmatic-snapshot-1234',
'/mnt/subvol2/.borgmatic-snapshot/mnt/subvol2/.borgmatic-snapshot',
Pattern_type.NO_RECURSE,
Pattern_style.FNMATCH,
),
Pattern(
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1/.borgmatic-snapshot-1234',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1/.borgmatic-snapshot',
Pattern_type.NO_RECURSE,
Pattern_style.FNMATCH,
),
Pattern('/foo'),
Pattern('/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1'),
Pattern('/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1/.cache', Pattern_type.EXCLUDE),
Pattern('/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1', Pattern_type.INCLUDE),
Pattern('/mnt/subvol2/.borgmatic-snapshot-1234/./mnt/subvol2'),
Pattern('/mnt/subvol2/.borgmatic-snapshot-1234/./mnt/subvol2', Pattern_type.INCLUDE),
Pattern('/mnt/subvol1/.borgmatic-snapshot/./mnt/subvol1'),
Pattern('/mnt/subvol1/.borgmatic-snapshot/./mnt/subvol1/.cache', Pattern_type.EXCLUDE),
Pattern('/mnt/subvol2/.borgmatic-snapshot/./mnt/subvol2'),
]
assert config == {
'btrfs': {},
@@ -85,7 +85,5 @@ def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
Pattern(
'/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir/.cache', Pattern_type.EXCLUDE
),
Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir', Pattern_type.INCLUDE),
Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'),
Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2', Pattern_type.INCLUDE),
]
@@ -2,7 +2,7 @@ import os
from flexmock import flexmock
from borgmatic.borg.pattern import Pattern, Pattern_type
from borgmatic.borg.pattern import Pattern
from borgmatic.hooks.data_source import zfs as module
@@ -44,5 +44,4 @@ def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
assert patterns == [
Pattern(os.path.join(snapshot_mount_path, 'subdir')),
Pattern(os.path.join(snapshot_mount_path, 'subdir'), Pattern_type.INCLUDE),
]
+6
View File
@@ -402,6 +402,12 @@ def test_device_map_patterns_with_existing_device_id_does_not_overwrite_it():
(Pattern('/', device=1),),
False,
),
(
(Pattern('/', device=1), Pattern('/other', device=1)),
'/root',
(Pattern('/', device=1), Pattern('/other', device=1)),
False,
),
(
(Pattern('/', device=1), Pattern('/root', device=2)),
None,
+42 -3
View File
@@ -101,7 +101,7 @@ def test_validate_planned_backup_paths_skips_borgmatic_runtime_directory():
)
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module).should_receive('any_parent_directories').replace_with(
lambda path, _: path == '/run/borgmatic/bar'
lambda path, candidates: any(path.startswith(parent) for parent in candidates)
)
assert module.validate_planned_backup_paths(
@@ -134,7 +134,7 @@ def test_validate_planned_backup_paths_with_borgmatic_runtime_directory_missing_
)
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module).should_receive('any_parent_directories').replace_with(
lambda path, _: path == '/run/borgmatic/bar'
lambda path, candidates: any(path.startswith(parent) for parent in candidates)
)
with pytest.raises(ValueError):
@@ -155,6 +155,45 @@ def test_validate_planned_backup_paths_with_borgmatic_runtime_directory_missing_
)
def test_validate_planned_backup_paths_with_borgmatic_runtime_directory_partially_excluded_from_paths_output_errors():
flexmock(module.flags).should_receive('omit_flag').replace_with(
lambda arguments, flag: arguments,
)
flexmock(module.flags).should_receive('omit_flag_and_value').replace_with(
lambda arguments, flag: arguments,
)
flexmock(module.environment).should_receive('make_environment').and_return(None)
# /run/borgmatic/bar is present, but /run/borgmatic/quux is missing.
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
'+ /foo\n- /run/borgmatic/bar\n- /baz',
)
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module).should_receive('any_parent_directories').replace_with(
lambda path, candidates: any(path.startswith(parent) for parent in candidates)
)
with pytest.raises(ValueError):
module.validate_planned_backup_paths(
dry_run=False,
create_command=('borg', 'create'),
config={},
patterns=(
module.borgmatic.borg.pattern.Pattern('/foo'),
module.borgmatic.borg.pattern.Pattern(
'/run/borgmatic/bar', module.borgmatic.borg.pattern.Pattern_type.ROOT
),
module.borgmatic.borg.pattern.Pattern('/baz'),
module.borgmatic.borg.pattern.Pattern(
'/run/borgmatic/quux', module.borgmatic.borg.pattern.Pattern_type.ROOT
),
),
local_path=None,
working_directory=None,
borgmatic_runtime_directory='/run/borgmatic',
)
def test_validate_planned_backup_paths_with_borgmatic_runtime_directory_missing_from_patterns_does_not_raise():
flexmock(module.flags).should_receive('omit_flag').replace_with(
lambda arguments, flag: arguments,
@@ -168,7 +207,7 @@ def test_validate_planned_backup_paths_with_borgmatic_runtime_directory_missing_
)
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module).should_receive('any_parent_directories').replace_with(
lambda path, _: path == '/run/borgmatic/bar'
lambda path, candidates: any(path.startswith(parent) for parent in candidates)
)
assert module.validate_planned_backup_paths(
+37 -17
View File
@@ -19,7 +19,7 @@ def test_make_list_command_includes_log_info():
repository_path='repo',
config={},
local_borg_version='1.2.3',
list_arguments=flexmock(archive=None, paths=None, json=False),
list_arguments=flexmock(archive=None, paths=None, format=None, json=False),
global_arguments=flexmock(),
)
@@ -36,7 +36,7 @@ def test_make_list_command_includes_json_but_not_info():
repository_path='repo',
config={},
local_borg_version='1.2.3',
list_arguments=flexmock(archive=None, paths=None, json=True),
list_arguments=flexmock(archive=None, paths=None, format=None, json=True),
global_arguments=flexmock(),
)
@@ -53,7 +53,7 @@ def test_make_list_command_includes_log_debug():
repository_path='repo',
config={},
local_borg_version='1.2.3',
list_arguments=flexmock(archive=None, paths=None, json=False),
list_arguments=flexmock(archive=None, paths=None, format=None, json=False),
global_arguments=flexmock(),
)
@@ -70,7 +70,7 @@ def test_make_list_command_includes_json_but_not_debug():
repository_path='repo',
config={},
local_borg_version='1.2.3',
list_arguments=flexmock(archive=None, paths=None, json=True),
list_arguments=flexmock(archive=None, paths=None, format=None, json=True),
global_arguments=flexmock(),
)
@@ -86,7 +86,7 @@ def test_make_list_command_includes_json():
repository_path='repo',
config={},
local_borg_version='1.2.3',
list_arguments=flexmock(archive=None, paths=None, json=True),
list_arguments=flexmock(archive=None, paths=None, format=None, json=True),
global_arguments=flexmock(),
)
@@ -96,7 +96,7 @@ def test_make_list_command_includes_json():
def test_make_list_command_includes_log_json():
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(()).and_return(
('--log-json',),
)
).and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
@@ -104,7 +104,7 @@ def test_make_list_command_includes_log_json():
repository_path='repo',
config={'log_json': True},
local_borg_version='1.2.3',
list_arguments=flexmock(archive=None, paths=None, json=False),
list_arguments=flexmock(archive=None, paths=None, format=None, json=False),
global_arguments=flexmock(),
)
@@ -114,7 +114,7 @@ def test_make_list_command_includes_log_json():
def test_make_list_command_includes_lock_wait():
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(()).and_return(
('--lock-wait', '5'),
)
).and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
@@ -122,13 +122,31 @@ def test_make_list_command_includes_lock_wait():
repository_path='repo',
config={'lock_wait': 5},
local_borg_version='1.2.3',
list_arguments=flexmock(archive=None, paths=None, json=False),
list_arguments=flexmock(archive=None, paths=None, format=None, json=False),
global_arguments=flexmock(),
)
assert command == ('borg', 'list', '--lock-wait', '5', 'repo')
def test_make_list_command_includes_format():
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(()).and_return(
()
).and_return(('--format', 'stuff'))
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
command = module.make_list_command(
repository_path='repo',
config={},
local_borg_version='1.2.3',
list_arguments=flexmock(archive=None, paths=None, format='stuff', json=False),
global_arguments=flexmock(),
)
assert command == ('borg', 'list', '--format', 'stuff', 'repo')
def test_make_list_command_includes_extra_borg_options():
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
@@ -138,7 +156,7 @@ def test_make_list_command_includes_extra_borg_options():
repository_path='repo',
config={'extra_borg_options': {'list': '--extra "value with space"'}},
local_borg_version='1.2.3',
list_arguments=flexmock(archive=None, paths=None, json=False),
list_arguments=flexmock(archive=None, paths=None, format=None, json=False),
global_arguments=flexmock(),
)
@@ -156,7 +174,7 @@ def test_make_list_command_includes_archive():
repository_path='repo',
config={},
local_borg_version='1.2.3',
list_arguments=flexmock(archive='archive', paths=None, json=False),
list_arguments=flexmock(archive='archive', paths=None, format=None, json=False),
global_arguments=flexmock(),
)
@@ -174,7 +192,7 @@ def test_make_list_command_includes_archive_and_path():
repository_path='repo',
config={},
local_borg_version='1.2.3',
list_arguments=flexmock(archive='archive', paths=['var/lib'], json=False),
list_arguments=flexmock(archive='archive', paths=['var/lib'], format=None, json=False),
global_arguments=flexmock(),
)
@@ -190,7 +208,7 @@ def test_make_list_command_includes_local_path():
repository_path='repo',
config={},
local_borg_version='1.2.3',
list_arguments=flexmock(archive=None, paths=None, json=False),
list_arguments=flexmock(archive=None, paths=None, format=None, json=False),
global_arguments=flexmock(),
local_path='borg2',
)
@@ -214,7 +232,7 @@ def test_make_list_command_includes_remote_path():
repository_path='repo',
config={},
local_borg_version='1.2.3',
list_arguments=flexmock(archive=None, paths=None, json=False),
list_arguments=flexmock(archive=None, paths=None, format=None, json=False),
global_arguments=flexmock(),
remote_path='borg2',
)
@@ -233,7 +251,7 @@ def test_make_list_command_includes_umask():
repository_path='repo',
config={'umask': '077'},
local_borg_version='1.2.3',
list_arguments=flexmock(archive=None, paths=None, json=False),
list_arguments=flexmock(archive=None, paths=None, format=None, json=False),
global_arguments=flexmock(),
)
@@ -249,7 +267,7 @@ def test_make_list_command_includes_short():
repository_path='repo',
config={},
local_borg_version='1.2.3',
list_arguments=flexmock(archive=None, paths=None, json=False, short=True),
list_arguments=flexmock(archive=None, paths=None, format=None, json=False, short=True),
global_arguments=flexmock(),
)
@@ -284,9 +302,9 @@ def test_make_list_command_includes_additional_flags(argument_name):
list_arguments=flexmock(
archive=None,
paths=None,
format=None,
json=False,
find_paths=None,
format=None,
**{argument_name: 'value'},
),
global_arguments=flexmock(),
@@ -344,6 +362,7 @@ def test_list_archive_calls_borg_with_flags():
list_arguments = argparse.Namespace(
archive='archive',
paths=None,
format=None,
json=False,
find_paths=None,
prefix=None,
@@ -410,6 +429,7 @@ def test_list_archive_calls_borg_with_local_path():
list_arguments = argparse.Namespace(
archive='archive',
paths=None,
format=None,
json=False,
find_paths=None,
prefix=None,
+59 -8
View File
@@ -600,6 +600,7 @@ def test_make_repo_list_command_includes_log_info():
repo_list_arguments=flexmock(
archive=None,
paths=None,
format=None,
json=False,
prefix=None,
match_archives=None,
@@ -629,6 +630,7 @@ def test_make_repo_list_command_includes_json_but_not_info():
repo_list_arguments=flexmock(
archive=None,
paths=None,
format=None,
json=True,
prefix=None,
match_archives=None,
@@ -658,6 +660,7 @@ def test_make_repo_list_command_includes_log_debug():
repo_list_arguments=flexmock(
archive=None,
paths=None,
format=None,
json=False,
prefix=None,
match_archives=None,
@@ -687,6 +690,7 @@ def test_make_repo_list_command_includes_json_but_not_debug():
repo_list_arguments=flexmock(
archive=None,
paths=None,
format=None,
json=True,
prefix=None,
match_archives=None,
@@ -715,6 +719,7 @@ def test_make_repo_list_command_includes_json():
repo_list_arguments=flexmock(
archive=None,
paths=None,
format=None,
json=True,
prefix=None,
match_archives=None,
@@ -729,7 +734,7 @@ def test_make_repo_list_command_includes_log_json():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(
('--log-json',),
).and_return(())
).and_return(()).and_return(())
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
None,
None,
@@ -745,6 +750,7 @@ def test_make_repo_list_command_includes_log_json():
repo_list_arguments=flexmock(
archive=None,
paths=None,
format=None,
json=False,
prefix=None,
match_archives=None,
@@ -759,7 +765,7 @@ def test_make_repo_list_command_includes_lock_wait():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(
('--lock-wait', '5'),
).and_return(())
).and_return(()).and_return(())
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
None,
None,
@@ -775,6 +781,7 @@ def test_make_repo_list_command_includes_lock_wait():
repo_list_arguments=flexmock(
archive=None,
paths=None,
format=None,
json=False,
prefix=None,
match_archives=None,
@@ -803,6 +810,7 @@ def test_make_repo_list_command_includes_list_extra_borg_options():
repo_list_arguments=flexmock(
archive=None,
paths=None,
format=None,
json=False,
prefix=None,
match_archives=None,
@@ -831,6 +839,7 @@ def test_make_repo_list_command_with_feature_available_includes_repo_list_extra_
repo_list_arguments=flexmock(
archive=None,
paths=None,
format=None,
json=False,
prefix=None,
match_archives=None,
@@ -859,6 +868,7 @@ def test_make_repo_list_command_includes_local_path():
repo_list_arguments=flexmock(
archive=None,
paths=None,
format=None,
json=False,
prefix=None,
match_archives=None,
@@ -890,6 +900,7 @@ def test_make_repo_list_command_includes_remote_path():
repo_list_arguments=flexmock(
archive=None,
paths=None,
format=None,
json=False,
prefix=None,
match_archives=None,
@@ -921,6 +932,7 @@ def test_make_repo_list_command_includes_umask():
repo_list_arguments=flexmock(
archive=None,
paths=None,
format=None,
json=False,
prefix=None,
match_archives=None,
@@ -935,7 +947,7 @@ def test_make_repo_list_command_transforms_prefix_into_match_archives():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(()).and_return(
('--match-archives', 'sh:foo*'),
)
).and_return(())
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
None,
None,
@@ -948,7 +960,9 @@ def test_make_repo_list_command_transforms_prefix_into_match_archives():
repository_path='repo',
config={},
local_borg_version='1.2.3',
repo_list_arguments=flexmock(archive=None, paths=None, json=False, prefix='foo'),
repo_list_arguments=flexmock(
archive=None, paths=None, format=None, json=False, prefix='foo'
),
global_arguments=flexmock(),
)
@@ -959,7 +973,7 @@ def test_make_repo_list_command_prefers_prefix_over_archive_name_format():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(()).and_return(
('--match-archives', 'sh:foo*'),
)
).and_return(())
flexmock(module.flags).should_receive('make_match_archives_flags').never()
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
@@ -968,7 +982,9 @@ def test_make_repo_list_command_prefers_prefix_over_archive_name_format():
repository_path='repo',
config={'archive_name_format': 'bar-{now}'},
local_borg_version='1.2.3',
repo_list_arguments=flexmock(archive=None, paths=None, json=False, prefix='foo'),
repo_list_arguments=flexmock(
archive=None, paths=None, format=None, json=False, prefix='foo'
),
global_arguments=flexmock(),
)
@@ -993,6 +1009,7 @@ def test_make_repo_list_command_transforms_archive_name_format_into_match_archiv
repo_list_arguments=flexmock(
archive=None,
paths=None,
format=None,
json=False,
prefix=None,
match_archives=None,
@@ -1003,6 +1020,38 @@ def test_make_repo_list_command_transforms_archive_name_format_into_match_archiv
assert command == ('borg', 'list', '--match-archives', 'sh:bar-*', 'repo')
def test_make_repo_list_command_includes_format_from_command_line():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(()).and_return(
('--format', 'stuff')
)
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
None,
None,
'1.2.3',
).and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
command = module.make_repo_list_command(
repository_path='repo',
config={},
local_borg_version='1.2.3',
repo_list_arguments=flexmock(
archive=None,
paths=None,
format='stuff',
json=False,
prefix=None,
match_archives=None,
short=False,
),
global_arguments=flexmock(),
)
assert command == ('borg', 'list', '--format', 'stuff', 'repo')
def test_make_repo_list_command_includes_short():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_flags').and_return(())
@@ -1021,6 +1070,7 @@ def test_make_repo_list_command_includes_short():
repo_list_arguments=flexmock(
archive=None,
paths=None,
format=None,
json=False,
prefix=None,
match_archives=None,
@@ -1064,11 +1114,11 @@ def test_make_repo_list_command_includes_additional_flags(argument_name):
repo_list_arguments=flexmock(
archive=None,
paths=None,
format=None,
json=False,
prefix=None,
match_archives=None,
find_paths=None,
format=None,
**{argument_name: 'value'},
),
global_arguments=flexmock(),
@@ -1100,11 +1150,11 @@ def test_make_repo_list_command_with_match_archives_calls_borg_with_match_archiv
repo_list_arguments=flexmock(
archive=None,
paths=None,
format=None,
json=False,
prefix=None,
match_archives='foo-*',
find_paths=None,
format=None,
),
global_arguments=flexmock(),
)
@@ -1173,6 +1223,7 @@ def test_make_repo_list_command_with_date_based_matching_calls_borg_with_date_ba
repo_list_arguments=flexmock(
archive=None,
paths=None,
format=None,
json=False,
prefix=None,
match_archives=None,
+143 -218
View File
@@ -5,36 +5,37 @@ from borgmatic.borg.pattern import Pattern, Pattern_source, Pattern_style, Patte
from borgmatic.hooks.data_source import btrfs as module
def test_path_is_a_subvolume_with_btrfs_success_call_returns_true():
def test_path_is_a_subvolume_with_btrfs_inode_returns_true():
module.path_is_a_subvolume.cache_clear()
flexmock(module.borgmatic.execute).should_receive(
'execute_command',
).with_args(('btrfs', 'subvolume', 'show', '/mnt0'), output_log_level=None, close_fds=True)
assert module.path_is_a_subvolume('btrfs', '/mnt0') is True
def test_path_is_a_subvolume_with_btrfs_error_returns_false():
module.path_is_a_subvolume.cache_clear()
flexmock(module.borgmatic.execute).should_receive(
'execute_command',
).with_args(
('btrfs', 'subvolume', 'show', '/mnt0'), output_log_level=None, close_fds=True
).and_raise(
module.subprocess.CalledProcessError(1, 'btrfs'),
flexmock(module.os).should_receive('stat').and_return(
flexmock(st_ino=module.BTRFS_SUBVOLUME_INODE_NUMBER)
)
assert module.path_is_a_subvolume('btrfs', '/mnt0') is False
assert module.path_is_a_subvolume('/mnt0') is True
def test_path_is_a_subvolume_with_non_btrfs_inode_returns_false():
module.path_is_a_subvolume.cache_clear()
flexmock(module.os).should_receive('stat').and_return(flexmock(st_ino=12345))
assert module.path_is_a_subvolume('/mnt0') is False
def test_path_is_a_subvolume_caches_result_after_first_call():
module.path_is_a_subvolume.cache_clear()
flexmock(module.borgmatic.execute).should_receive(
'execute_command',
flexmock(module.os).should_receive('stat').and_return(
flexmock(st_ino=module.BTRFS_SUBVOLUME_INODE_NUMBER)
).once()
assert module.path_is_a_subvolume('btrfs', '/mnt0') is True
assert module.path_is_a_subvolume('btrfs', '/mnt0') is True
assert module.path_is_a_subvolume('/mnt0') is True
assert module.path_is_a_subvolume('/mnt0') is True
def test_path_is_a_subvolume_with_missing_path_returns_false():
module.path_is_a_subvolume.cache_clear()
flexmock(module.os).should_receive('stat').and_raise(FileNotFoundError)
assert module.path_is_a_subvolume('/mnt0') is False
def test_get_subvolume_property_with_invalid_btrfs_output_errors():
@@ -85,76 +86,66 @@ def test_get_subvolume_property_caches_result_after_first_call():
def test_get_containing_subvolume_path_with_subvolume_self_returns_it():
flexmock(module).should_receive('path_is_a_subvolume').with_args(
'btrfs', '/foo/bar/baz'
).and_return(True)
flexmock(module).should_receive('path_is_a_subvolume').with_args('btrfs', '/foo/bar').never()
flexmock(module).should_receive('path_is_a_subvolume').with_args('btrfs', '/foo').never()
flexmock(module).should_receive('path_is_a_subvolume').with_args('btrfs', '/').never()
flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo/bar/baz').and_return(
True
)
flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo/bar').never()
flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo').never()
flexmock(module).should_receive('path_is_a_subvolume').with_args('/').never()
flexmock(module).should_receive('get_subvolume_property').and_return(False)
assert module.get_containing_subvolume_path('btrfs', '/foo/bar/baz') == '/foo/bar/baz'
def test_get_containing_subvolume_path_with_subvolume_parent_returns_it():
flexmock(module).should_receive('path_is_a_subvolume').with_args(
'btrfs', '/foo/bar/baz'
).and_return(False)
flexmock(module).should_receive('path_is_a_subvolume').with_args(
'btrfs', '/foo/bar'
).and_return(True)
flexmock(module).should_receive('path_is_a_subvolume').with_args('btrfs', '/foo').never()
flexmock(module).should_receive('path_is_a_subvolume').with_args('btrfs', '/').never()
flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo/bar/baz').and_return(
False
)
flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo/bar').and_return(True)
flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo').never()
flexmock(module).should_receive('path_is_a_subvolume').with_args('/').never()
flexmock(module).should_receive('get_subvolume_property').and_return(False)
assert module.get_containing_subvolume_path('btrfs', '/foo/bar/baz') == '/foo/bar'
def test_get_containing_subvolume_path_with_subvolume_grandparent_returns_it():
flexmock(module).should_receive('path_is_a_subvolume').with_args(
'btrfs', '/foo/bar/baz'
).and_return(False)
flexmock(module).should_receive('path_is_a_subvolume').with_args(
'btrfs', '/foo/bar'
).and_return(False)
flexmock(module).should_receive('path_is_a_subvolume').with_args('btrfs', '/foo').and_return(
True
flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo/bar/baz').and_return(
False
)
flexmock(module).should_receive('path_is_a_subvolume').with_args('btrfs', '/').never()
flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo/bar').and_return(False)
flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo').and_return(True)
flexmock(module).should_receive('path_is_a_subvolume').with_args('/').never()
flexmock(module).should_receive('get_subvolume_property').and_return(False)
assert module.get_containing_subvolume_path('btrfs', '/foo/bar/baz') == '/foo'
def test_get_containing_subvolume_path_without_subvolume_ancestor_returns_none():
flexmock(module).should_receive('path_is_a_subvolume').with_args(
'btrfs', '/foo/bar/baz'
).and_return(False)
flexmock(module).should_receive('path_is_a_subvolume').with_args(
'btrfs', '/foo/bar'
).and_return(False)
flexmock(module).should_receive('path_is_a_subvolume').with_args('btrfs', '/foo').and_return(
flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo/bar/baz').and_return(
False
)
flexmock(module).should_receive('path_is_a_subvolume').with_args('btrfs', '/').and_return(False)
flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo/bar').and_return(False)
flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo').and_return(False)
flexmock(module).should_receive('path_is_a_subvolume').with_args('/').and_return(False)
flexmock(module).should_receive('get_subvolume_property').and_return(False)
assert module.get_containing_subvolume_path('btrfs', '/foo/bar/baz') is None
def test_get_containing_subvolume_path_with_read_only_subvolume_returns_none():
flexmock(module).should_receive('path_is_a_subvolume').with_args(
'btrfs', '/foo/bar/baz'
).and_return(True)
flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo/bar/baz').and_return(
True
)
flexmock(module).should_receive('get_subvolume_property').and_return(True)
assert module.get_containing_subvolume_path('btrfs', '/foo/bar/baz') is None
def test_get_containing_subvolume_path_with_read_only_error_returns_none():
flexmock(module).should_receive('path_is_a_subvolume').with_args(
'btrfs', '/foo/bar/baz'
).and_return(True)
flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo/bar/baz').and_return(
True
)
flexmock(module).should_receive('get_subvolume_property').and_raise(
module.subprocess.CalledProcessError(1, 'wtf')
)
@@ -340,16 +331,14 @@ def test_get_subvolumes_skips_non_config_patterns():
@pytest.mark.parametrize(
'subvolume_path,expected_snapshot_path',
(
('/foo/bar', '/foo/bar/.borgmatic-snapshot-1234/foo/bar'),
('/', '/.borgmatic-snapshot-1234'),
('/foo/bar', '/foo/bar/.borgmatic-snapshot/foo/bar'),
('/', '/.borgmatic-snapshot'),
),
)
def test_make_snapshot_path_includes_stripped_subvolume_path(
subvolume_path,
expected_snapshot_path,
):
flexmock(module.os).should_receive('getpid').and_return(1234)
assert module.make_snapshot_path(subvolume_path) == expected_snapshot_path
@@ -359,14 +348,14 @@ def test_make_snapshot_path_includes_stripped_subvolume_path(
(
'/foo/bar',
Pattern('/foo/bar/baz'),
Pattern('/foo/bar/.borgmatic-snapshot-1234/./foo/bar/baz'),
Pattern('/foo/bar/.borgmatic-snapshot/./foo/bar/baz'),
),
('/foo/bar', Pattern('/foo/bar'), Pattern('/foo/bar/.borgmatic-snapshot-1234/./foo/bar')),
('/foo/bar', Pattern('/foo/bar'), Pattern('/foo/bar/.borgmatic-snapshot/./foo/bar')),
(
'/foo/bar',
Pattern('^/foo/bar', Pattern_type.INCLUDE, Pattern_style.REGULAR_EXPRESSION),
Pattern(
'^/foo/bar/.borgmatic-snapshot-1234/./foo/bar',
'^/foo/bar/.borgmatic-snapshot/./foo/bar',
Pattern_type.INCLUDE,
Pattern_style.REGULAR_EXPRESSION,
),
@@ -375,17 +364,17 @@ def test_make_snapshot_path_includes_stripped_subvolume_path(
'/foo/bar',
Pattern('/foo/bar', Pattern_type.INCLUDE, Pattern_style.REGULAR_EXPRESSION),
Pattern(
'/foo/bar/.borgmatic-snapshot-1234/./foo/bar',
'/foo/bar/.borgmatic-snapshot/./foo/bar',
Pattern_type.INCLUDE,
Pattern_style.REGULAR_EXPRESSION,
),
),
('/', Pattern('/foo'), Pattern('/.borgmatic-snapshot-1234/./foo')),
('/', Pattern('/'), Pattern('/.borgmatic-snapshot-1234/./')),
('/', Pattern('/foo'), Pattern('/.borgmatic-snapshot/./foo')),
('/', Pattern('/'), Pattern('/.borgmatic-snapshot/./')),
(
'/foo/bar',
Pattern('/foo/bar/./baz'),
Pattern('/foo/bar/.borgmatic-snapshot-1234/foo/bar/./baz'),
Pattern('/foo/bar/.borgmatic-snapshot/foo/bar/./baz'),
),
),
)
@@ -394,8 +383,6 @@ def test_make_borg_snapshot_pattern_includes_slashdot_hack_and_stripped_pattern_
pattern,
expected_pattern,
):
flexmock(module.os).should_receive('getpid').and_return(1234)
assert module.make_borg_snapshot_pattern(subvolume_path, pattern) == expected_pattern
@@ -409,26 +396,26 @@ def test_dump_data_sources_snapshots_each_subvolume_and_replaces_patterns():
),
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
'/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/mnt/subvol2',
)
flexmock(module).should_receive('snapshot_subvolume').with_args(
'btrfs',
'/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
).once()
flexmock(module).should_receive('snapshot_subvolume').with_args(
'btrfs',
'/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/mnt/subvol2',
).once()
flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
'/mnt/subvol1',
).and_return(
Pattern(
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1/.borgmatic-snapshot-1234',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1/.borgmatic-snapshot',
Pattern_type.NO_RECURSE,
Pattern_style.FNMATCH,
),
@@ -437,7 +424,7 @@ def test_dump_data_sources_snapshots_each_subvolume_and_replaces_patterns():
'/mnt/subvol2',
).and_return(
Pattern(
'/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2/.borgmatic-snapshot-1234',
'/mnt/subvol2/.borgmatic-snapshot/mnt/subvol2/.borgmatic-snapshot',
Pattern_type.NO_RECURSE,
Pattern_style.FNMATCH,
),
@@ -445,36 +432,31 @@ def test_dump_data_sources_snapshots_each_subvolume_and_replaces_patterns():
flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
'/mnt/subvol1',
object,
).and_return(Pattern('/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1'))
).and_return(Pattern('/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1'))
flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
'/mnt/subvol2',
object,
).and_return(Pattern('/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2'))
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).and_return(0)
).and_return(Pattern('/mnt/subvol2/.borgmatic-snapshot/mnt/subvol2'))
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
Pattern('/mnt/subvol1'),
module.borgmatic.borg.pattern.Pattern(
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
Pattern('/mnt/subvol2'),
module.borgmatic.borg.pattern.Pattern(
'/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/mnt/subvol2',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
object,
module.borgmatic.borg.pattern.Pattern(
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1/.borgmatic-snapshot-1234',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1/.borgmatic-snapshot',
Pattern_type.NO_RECURSE,
Pattern_style.FNMATCH,
),
@@ -482,7 +464,7 @@ def test_dump_data_sources_snapshots_each_subvolume_and_replaces_patterns():
flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
object,
module.borgmatic.borg.pattern.Pattern(
'/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2/.borgmatic-snapshot-1234',
'/mnt/subvol2/.borgmatic-snapshot/mnt/subvol2/.borgmatic-snapshot',
Pattern_type.NO_RECURSE,
Pattern_style.FNMATCH,
),
@@ -512,18 +494,18 @@ def test_dump_data_sources_uses_custom_btrfs_command_in_commands():
(module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),),
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
)
flexmock(module).should_receive('snapshot_subvolume').with_args(
'/usr/local/bin/btrfs',
'/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
).once()
flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
'/mnt/subvol1',
).and_return(
Pattern(
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1/.borgmatic-snapshot-1234',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1/.borgmatic-snapshot',
Pattern_type.NO_RECURSE,
Pattern_style.FNMATCH,
),
@@ -531,23 +513,19 @@ def test_dump_data_sources_uses_custom_btrfs_command_in_commands():
flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
'/mnt/subvol1',
object,
).and_return(Pattern('/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1'))
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).and_return(0)
).and_return(Pattern('/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1'))
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
Pattern('/mnt/subvol1'),
module.borgmatic.borg.pattern.Pattern(
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
object,
module.borgmatic.borg.pattern.Pattern(
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1/.borgmatic-snapshot-1234',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1/.borgmatic-snapshot',
Pattern_type.NO_RECURSE,
Pattern_style.FNMATCH,
),
@@ -583,18 +561,18 @@ def test_dump_data_sources_with_findmnt_command_warns():
(module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),),
).once()
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
)
flexmock(module).should_receive('snapshot_subvolume').with_args(
'btrfs',
'/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
).once()
flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
'/mnt/subvol1',
).and_return(
Pattern(
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1/.borgmatic-snapshot-1234',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1/.borgmatic-snapshot',
Pattern_type.NO_RECURSE,
Pattern_style.FNMATCH,
),
@@ -602,23 +580,19 @@ def test_dump_data_sources_with_findmnt_command_warns():
flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
'/mnt/subvol1',
object,
).and_return(Pattern('/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1'))
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).and_return(0)
).and_return(Pattern('/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1'))
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
Pattern('/mnt/subvol1'),
module.borgmatic.borg.pattern.Pattern(
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
object,
module.borgmatic.borg.pattern.Pattern(
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1/.borgmatic-snapshot-1234',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1/.borgmatic-snapshot',
Pattern_type.NO_RECURSE,
Pattern_style.FNMATCH,
),
@@ -650,13 +624,10 @@ def test_dump_data_sources_with_dry_run_skips_snapshot_and_patterns_update():
(module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),),
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
)
flexmock(module).should_receive('snapshot_subvolume').never()
flexmock(module).should_receive('make_snapshot_exclude_pattern').never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').never()
@@ -682,9 +653,6 @@ def test_dump_data_sources_without_matching_subvolumes_skips_snapshot_and_patter
flexmock(module).should_receive('make_snapshot_path').never()
flexmock(module).should_receive('snapshot_subvolume').never()
flexmock(module).should_receive('make_snapshot_exclude_pattern').never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').never()
@@ -712,91 +680,59 @@ def test_remove_data_source_dumps_deletes_snapshots():
),
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
'/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/./mnt/subvol1',
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
'/mnt/subvol2/.borgmatic-snapshot-1234/./mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/./mnt/subvol2',
)
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob',
).with_args(
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/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-snapshot-1234/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/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-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
),
('/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',),
)
flexmock(module.glob).should_receive('glob').with_args(
'/mnt/subvol2/.borgmatic-*/mnt/subvol2',
).and_return(
(
'/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
),
('/mnt/subvol2/.borgmatic-snapshot/mnt/subvol2',),
)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
'/mnt/subvol2/.borgmatic-snapshot/mnt/subvol2',
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
).and_return(False)
flexmock(module).should_receive('delete_snapshot').with_args(
'btrfs',
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
).once()
flexmock(module).should_receive('delete_snapshot').with_args(
'btrfs',
'/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
'/mnt/subvol2/.borgmatic-snapshot/mnt/subvol2',
).once()
flexmock(module).should_receive('delete_snapshot').with_args(
'btrfs',
'/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
).once()
flexmock(module).should_receive('delete_snapshot').with_args(
'btrfs',
'/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
).never()
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol1/.borgmatic-snapshot-1234',
'/mnt/subvol1/.borgmatic-snapshot',
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol1/.borgmatic-snapshot-5678',
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol2/.borgmatic-snapshot-1234',
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol2/.borgmatic-snapshot-5678',
'/mnt/subvol2/.borgmatic-snapshot',
).and_return(True)
flexmock(module.shutil).should_receive('rmtree').with_args(
'/mnt/subvol1/.borgmatic-snapshot-1234',
'/mnt/subvol1/.borgmatic-snapshot',
).once()
flexmock(module.shutil).should_receive('rmtree').with_args(
'/mnt/subvol1/.borgmatic-snapshot-5678',
'/mnt/subvol2/.borgmatic-snapshot',
).once()
flexmock(module.shutil).should_receive('rmtree').with_args(
'/mnt/subvol2/.borgmatic-snapshot-1234',
).once()
flexmock(module.shutil).should_receive('rmtree').with_args(
'/mnt/subvol2/.borgmatic-snapshot-5678',
).never()
module.remove_data_source_dumps(
hook_config=config['btrfs'],
@@ -874,50 +810,50 @@ def test_remove_data_source_dumps_with_dry_run_skips_deletes():
),
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
'/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/./mnt/subvol1',
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
'/mnt/subvol2/.borgmatic-snapshot-1234/./mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/./mnt/subvol2',
)
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob',
).with_args(
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/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-snapshot-1234/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/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-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
),
)
flexmock(module.glob).should_receive('glob').with_args(
'/mnt/subvol2/.borgmatic-*/mnt/subvol2',
).and_return(
(
'/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/mnt/subvol2',
),
)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/mnt/subvol2',
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/mnt/subvol2',
).and_return(False)
flexmock(module).should_receive('delete_snapshot').never()
flexmock(module.shutil).should_receive('rmtree').never()
@@ -959,21 +895,21 @@ def test_remove_data_source_without_snapshots_skips_deletes():
),
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
'/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/./mnt/subvol1',
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
'/mnt/subvol2/.borgmatic-snapshot-1234/./mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/./mnt/subvol2',
)
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob',
).with_args(
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/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-snapshot-1234/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/mnt/subvol2',
temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
).and_return('/mnt/subvol2/.borgmatic-*/mnt/subvol2')
flexmock(module.glob).should_receive('glob').and_return(())
@@ -999,50 +935,50 @@ def test_remove_data_source_dumps_with_delete_snapshot_file_not_found_error_bail
),
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
'/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/./mnt/subvol1',
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
'/mnt/subvol2/.borgmatic-snapshot-1234/./mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/./mnt/subvol2',
)
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob',
).with_args(
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/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-snapshot-1234/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/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-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
),
)
flexmock(module.glob).should_receive('glob').with_args(
'/mnt/subvol2/.borgmatic-*/mnt/subvol2',
).and_return(
(
'/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/mnt/subvol2',
),
)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/mnt/subvol2',
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/mnt/subvol2',
).and_return(False)
flexmock(module).should_receive('delete_snapshot').and_raise(FileNotFoundError)
flexmock(module.shutil).should_receive('rmtree').never()
@@ -1065,50 +1001,50 @@ def test_remove_data_source_dumps_with_delete_snapshot_called_process_error_bail
),
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
'/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/./mnt/subvol1',
)
flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
'/mnt/subvol2/.borgmatic-snapshot-1234/./mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/./mnt/subvol2',
)
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob',
).with_args(
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/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-snapshot-1234/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/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-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
),
)
flexmock(module.glob).should_receive('glob').with_args(
'/mnt/subvol2/.borgmatic-*/mnt/subvol2',
).and_return(
(
'/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/mnt/subvol2',
),
)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
'/mnt/subvol1/.borgmatic-snapshot/mnt/subvol1',
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/mnt/subvol2',
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
'/mnt/subvol2/.borgmatic-snapshot/mnt/subvol2',
).and_return(False)
flexmock(module).should_receive('delete_snapshot').and_raise(
module.subprocess.CalledProcessError(1, 'command', 'error'),
@@ -1131,40 +1067,29 @@ def test_remove_data_source_dumps_with_root_subvolume_skips_duplicate_removal():
)
flexmock(module).should_receive('make_snapshot_path').with_args('/').and_return(
'/.borgmatic-snapshot-1234',
'/.borgmatic-snapshot',
)
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob',
).with_args(
'/.borgmatic-snapshot-1234',
'/.borgmatic-snapshot',
temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
).and_return('/.borgmatic-*')
flexmock(module.glob).should_receive('glob').with_args('/.borgmatic-*').and_return(
('/.borgmatic-snapshot-1234', '/.borgmatic-snapshot-5678'),
('/.borgmatic-snapshot', '/.borgmatic-snapshot'),
)
flexmock(module.os.path).should_receive('isdir').with_args(
'/.borgmatic-snapshot-1234'
).and_return(
True,
).and_return(False)
flexmock(module.os.path).should_receive('isdir').with_args(
'/.borgmatic-snapshot-5678'
).and_return(
True,
flexmock(module.os.path).should_receive('isdir').with_args('/.borgmatic-snapshot').and_return(
True
).and_return(False)
flexmock(module).should_receive('delete_snapshot').with_args(
'btrfs', '/.borgmatic-snapshot-1234'
).once()
flexmock(module).should_receive('delete_snapshot').with_args(
'btrfs', '/.borgmatic-snapshot-5678'
'btrfs', '/.borgmatic-snapshot'
).once()
flexmock(module.os.path).should_receive('isdir').with_args('').and_return(False)
flexmock(module.shutil).should_receive('rmtree').never()
module.remove_data_source_dumps(
+15 -69
View File
@@ -203,50 +203,23 @@ def test_inject_pattern_with_root_pattern_prepends_it_along_with_corresponding_i
]
def test_get_last_pattern_index_with_ordered_subset_patterns_finds_last_one():
def test_inject_pattern_with_root_pattern_and_override_excludes_false_omits_include_pattern():
patterns = [
module.borgmatic.borg.pattern.Pattern('/foo'),
module.borgmatic.borg.pattern.Pattern('/bar'),
module.borgmatic.borg.pattern.Pattern('/baz'),
module.borgmatic.borg.pattern.Pattern('/quux'),
]
patterns_subset = [
module.borgmatic.borg.pattern.Pattern('/bar'),
module.borgmatic.borg.pattern.Pattern('/baz'),
module.borgmatic.borg.pattern.Pattern('/etc'),
module.borgmatic.borg.pattern.Pattern('/var'),
]
assert module.get_last_pattern_index(patterns, patterns_subset) == 2
module.inject_pattern(
patterns,
module.borgmatic.borg.pattern.Pattern('/foo/bar'),
override_excludes=False,
)
def test_get_last_pattern_index_with_unordered_subset_patterns_finds_last_one():
patterns = [
module.borgmatic.borg.pattern.Pattern('/foo'),
module.borgmatic.borg.pattern.Pattern('/bar'),
module.borgmatic.borg.pattern.Pattern('/baz'),
module.borgmatic.borg.pattern.Pattern('/quux'),
assert patterns == [
module.borgmatic.borg.pattern.Pattern('/foo/bar'),
module.borgmatic.borg.pattern.Pattern('/etc'),
module.borgmatic.borg.pattern.Pattern('/var'),
]
patterns_subset = [
module.borgmatic.borg.pattern.Pattern('/baz'),
module.borgmatic.borg.pattern.Pattern('/bar'),
]
assert module.get_last_pattern_index(patterns, patterns_subset) == 2
def test_get_last_pattern_index_with_unknown_subset_patterns_skips_it():
patterns = [
module.borgmatic.borg.pattern.Pattern('/foo'),
module.borgmatic.borg.pattern.Pattern('/bar'),
module.borgmatic.borg.pattern.Pattern('/baz'),
module.borgmatic.borg.pattern.Pattern('/quux'),
]
patterns_subset = [
module.borgmatic.borg.pattern.Pattern('/baz'),
module.borgmatic.borg.pattern.Pattern('/unknown'),
module.borgmatic.borg.pattern.Pattern('/bar'),
]
assert module.get_last_pattern_index(patterns, patterns_subset) == 2
def test_replace_pattern_swaps_out_pattern_in_place():
@@ -263,7 +236,6 @@ def test_replace_pattern_swaps_out_pattern_in_place():
'/foo/bar',
type=module.borgmatic.borg.pattern.Pattern_type.EXCLUDE,
),
0,
)
assert patterns == [
@@ -283,39 +255,13 @@ def test_replace_pattern_with_unknown_pattern_falls_back_to_injecting():
module.borgmatic.borg.pattern.Pattern('/lib'),
]
flexmock(module).should_receive('inject_pattern').with_args(
patterns, module.borgmatic.borg.pattern.Pattern('/foo/bar')
patterns,
module.borgmatic.borg.pattern.Pattern('/foo/bar'),
override_excludes=False,
).once()
module.replace_pattern(
patterns,
module.borgmatic.borg.pattern.Pattern('/unknown'),
module.borgmatic.borg.pattern.Pattern('/foo/bar'),
0,
)
def test_replace_pattern_with_root_pattern_swaps_it_in_along_with_corresponding_include_pattern():
patterns = [
module.borgmatic.borg.pattern.Pattern('/etc'),
module.borgmatic.borg.pattern.Pattern('/var'),
module.borgmatic.borg.pattern.Pattern('/lib'),
module.borgmatic.borg.pattern.Pattern('/run'),
]
module.replace_pattern(
patterns,
module.borgmatic.borg.pattern.Pattern('/var'),
module.borgmatic.borg.pattern.Pattern('/foo/bar'),
2,
)
assert patterns == [
module.borgmatic.borg.pattern.Pattern('/etc'),
module.borgmatic.borg.pattern.Pattern('/foo/bar'),
module.borgmatic.borg.pattern.Pattern('/lib'),
module.borgmatic.borg.pattern.Pattern(
'/foo/bar',
type=module.borgmatic.borg.pattern.Pattern_type.INCLUDE,
),
module.borgmatic.borg.pattern.Pattern('/run'),
]
-29
View File
@@ -360,9 +360,6 @@ def test_dump_data_sources_snapshots_and_mounts_and_replaces_patterns():
logical_volumes[1],
'/run/borgmatic',
).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'))
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).and_return(0)
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
Pattern('/mnt/lvolume1/subdir'),
@@ -370,7 +367,6 @@ def test_dump_data_sources_snapshots_and_mounts_and_replaces_patterns():
'/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
@@ -379,7 +375,6 @@ def test_dump_data_sources_snapshots_and_mounts_and_replaces_patterns():
'/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
assert (
@@ -401,9 +396,6 @@ def test_dump_data_sources_with_no_logical_volumes_skips_snapshots():
flexmock(module).should_receive('get_logical_volumes').and_return(())
flexmock(module).should_receive('snapshot_logical_volume').never()
flexmock(module).should_receive('mount_snapshot').never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).and_return(0)
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
assert (
@@ -485,9 +477,6 @@ def test_dump_data_sources_uses_snapshot_size_for_snapshot():
logical_volumes[1],
'/run/borgmatic',
).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'))
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).and_return(0)
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
Pattern('/mnt/lvolume1/subdir'),
@@ -495,7 +484,6 @@ def test_dump_data_sources_uses_snapshot_size_for_snapshot():
'/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
@@ -504,7 +492,6 @@ def test_dump_data_sources_uses_snapshot_size_for_snapshot():
'/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
assert (
@@ -593,9 +580,6 @@ def test_dump_data_sources_uses_custom_commands():
logical_volumes[1],
'/run/borgmatic',
).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'))
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).and_return(0)
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
Pattern('/mnt/lvolume1/subdir'),
@@ -603,7 +587,6 @@ def test_dump_data_sources_uses_custom_commands():
'/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
@@ -612,7 +595,6 @@ def test_dump_data_sources_uses_custom_commands():
'/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
assert (
@@ -651,9 +633,6 @@ def test_dump_data_sources_with_dry_run_skips_snapshots_and_does_not_touch_patte
flexmock(module).should_receive('snapshot_logical_volume').never()
flexmock(module).should_receive('get_snapshots').never()
flexmock(module).should_receive('mount_snapshot').never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
assert (
@@ -735,9 +714,6 @@ def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained
logical_volumes[1],
'/run/borgmatic',
).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'))
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).and_return(0)
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
Pattern('/mnt/lvolume1/subdir'),
@@ -745,7 +721,6 @@ def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained
'/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
@@ -754,7 +729,6 @@ def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained
'/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
assert (
@@ -811,9 +785,6 @@ def test_dump_data_sources_with_missing_snapshot_errors():
snapshot_name='lvolume2_borgmatic-1234',
).never()
flexmock(module).should_receive('mount_snapshot').never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
with pytest.raises(ValueError):
@@ -181,6 +181,7 @@ def test_database_names_to_dump_queries_mariadb_for_database_names():
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root',
'trustsome1',
@@ -220,6 +221,7 @@ def test_database_names_to_dump_with_database_name_all_and_skip_names_filters_ou
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root',
'trustsome1',
@@ -251,12 +253,53 @@ def test_database_names_to_dump_with_database_name_all_and_skip_names_filters_ou
assert names == ('baz',)
def test_database_names_to_dump_runs_mariadb_with_socket_path():
environment = flexmock()
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root',
'trustsome1',
None,
).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
(
'mariadb',
'--defaults-extra-file=/dev/fd/99',
'--socket',
'/socket',
'--skip-column-names',
'--batch',
'--execute',
'show schemas',
),
environment=environment,
working_directory=None,
).and_return('foo\nbar\nmysql\n').once()
names = module.database_names_to_dump(
{'name': 'all', 'socket_path': '/socket'},
{},
'root',
'trustsome1',
environment,
dry_run=False,
)
assert names == ('foo', 'bar')
def test_database_names_to_dump_with_environment_password_transport_skips_defaults_file_and_passes_user_flag():
environment = flexmock()
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module).should_receive('make_defaults_file_options').never()
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
@@ -291,6 +334,7 @@ def test_database_names_to_dump_runs_mariadb_with_tls():
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root',
'trustsome1',
@@ -329,6 +373,7 @@ def test_database_names_to_dump_runs_mariadb_without_tls():
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root',
'trustsome1',
@@ -706,6 +751,7 @@ def test_database_names_to_dump_runs_mariadb_with_list_options():
('--skip-ssl',),
'mariadb.cnf',
)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root',
'trustsome1',
@@ -742,6 +788,7 @@ def test_database_names_to_dump_runs_non_default_mariadb_with_list_options():
('--skip-ssl',),
'mariadb.cnf',
)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root',
'trustsome1',
@@ -776,6 +823,7 @@ def test_execute_dump_command_runs_mariadb_dump():
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root',
'trustsome1',
@@ -789,6 +837,7 @@ def test_execute_dump_command_runs_mariadb_dump():
'mariadb-dump',
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--single-transaction',
'--databases',
'foo',
'--result-file',
@@ -823,6 +872,7 @@ def test_execute_dump_command_with_environment_password_transport_skips_defaults
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module).should_receive('make_defaults_file_options').never()
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
@@ -831,6 +881,7 @@ def test_execute_dump_command_with_environment_password_transport_skips_defaults
(
'mariadb-dump',
'--add-drop-database',
'--single-transaction',
'--user',
'root',
'--databases',
@@ -867,6 +918,7 @@ def test_execute_dump_command_runs_mariadb_dump_without_add_drop_database():
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root',
'trustsome1',
@@ -879,6 +931,7 @@ def test_execute_dump_command_runs_mariadb_dump_without_add_drop_database():
(
'mariadb-dump',
'--defaults-extra-file=/dev/fd/99',
'--single-transaction',
'--databases',
'foo',
'--result-file',
@@ -913,6 +966,9 @@ def test_execute_dump_command_runs_mariadb_dump_with_hostname_and_port():
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(
'database.example.org'
)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root',
'trustsome1',
@@ -926,6 +982,7 @@ def test_execute_dump_command_runs_mariadb_dump_with_hostname_and_port():
'mariadb-dump',
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--single-transaction',
'--host',
'database.example.org',
'--port',
@@ -966,6 +1023,7 @@ def test_execute_dump_command_runs_mariadb_dump_with_tls():
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root',
'trustsome1',
@@ -979,6 +1037,7 @@ def test_execute_dump_command_runs_mariadb_dump_with_tls():
'mariadb-dump',
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--single-transaction',
'--ssl',
'--databases',
'foo',
@@ -1014,6 +1073,7 @@ def test_execute_dump_command_runs_mariadb_dump_without_tls():
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root',
'trustsome1',
@@ -1027,6 +1087,7 @@ def test_execute_dump_command_runs_mariadb_dump_without_tls():
'mariadb-dump',
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--single-transaction',
'--skip-ssl',
'--databases',
'foo',
@@ -1062,6 +1123,7 @@ def test_execute_dump_command_runs_mariadb_dump_with_username_and_password():
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root',
'trustsome1',
@@ -1075,6 +1137,7 @@ def test_execute_dump_command_runs_mariadb_dump_with_username_and_password():
'mariadb-dump',
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--single-transaction',
'--databases',
'foo',
'--result-file',
@@ -1109,6 +1172,7 @@ def test_execute_dump_command_runs_mariadb_dump_with_options():
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return(('--stuff=such',), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root',
'trustsome1',
@@ -1123,6 +1187,7 @@ def test_execute_dump_command_runs_mariadb_dump_with_options():
'--defaults-extra-file=/dev/fd/99',
'--stuff=such',
'--add-drop-database',
'--single-transaction',
'--databases',
'foo',
'--result-file',
@@ -1157,6 +1222,7 @@ def test_execute_dump_command_runs_non_default_mariadb_dump_with_options():
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return(('--stuff=such',), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root',
'trustsome1',
@@ -1171,6 +1237,7 @@ def test_execute_dump_command_runs_non_default_mariadb_dump_with_options():
'--defaults-extra-file=/dev/fd/99',
'--stuff=such',
'--add-drop-database',
'--single-transaction',
'--databases',
'foo',
'--result-file',
@@ -1205,6 +1272,7 @@ def test_execute_dump_command_with_duplicate_dump_skips_mariadb_dump():
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root',
'trustsome1',
@@ -1236,6 +1304,7 @@ def test_execute_dump_command_with_dry_run_skips_mariadb_dump():
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root',
'trustsome1',
@@ -1269,6 +1338,7 @@ def test_restore_data_source_dump_runs_mariadb_to_restore():
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
None,
None,
@@ -1309,6 +1379,7 @@ def test_restore_data_source_dump_runs_mariadb_with_options():
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return(('--harder',), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
None,
None,
@@ -1351,6 +1422,7 @@ def test_restore_data_source_dump_runs_non_default_mariadb_with_options():
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return(('--harder',), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
None,
None,
@@ -1391,6 +1463,9 @@ def test_restore_data_source_dump_runs_mariadb_with_hostname_and_port():
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').replace_with(
lambda option, data_source, connection_params=None, restore=False: data_source.get(option)
)
flexmock(module).should_receive('make_defaults_file_options').with_args(
None,
None,
@@ -1432,6 +1507,54 @@ def test_restore_data_source_dump_runs_mariadb_with_hostname_and_port():
)
def test_restore_data_source_dump_runs_mariadb_with_socket_path():
hook_config = [{'name': 'foo', 'socket_path': '/socket'}]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').replace_with(
lambda option, data_source, connection_params=None, restore=False: data_source.get(option)
)
flexmock(module).should_receive('make_defaults_file_options').with_args(
None,
None,
None,
).and_return(())
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mariadb',
'--batch',
'--socket',
'/socket',
),
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
environment={'USER': 'root'},
working_directory=None,
).once()
module.restore_data_source_dump(
hook_config,
{},
data_source=hook_config[0],
dry_run=False,
extract_process=extract_process,
connection_params={
'hostname': None,
'port': None,
'username': None,
'password': None,
},
borgmatic_runtime_directory='/run/borgmatic',
)
def test_restore_data_source_dump_runs_mariadb_with_tls():
hook_config = [{'name': 'foo', 'tls': True}]
extract_process = flexmock(stdout=flexmock())
@@ -1440,6 +1563,9 @@ def test_restore_data_source_dump_runs_mariadb_with_tls():
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').replace_with(
lambda option, data_source, connection_params=None, restore=False: data_source.get(option)
)
flexmock(module).should_receive('make_defaults_file_options').with_args(
None,
None,
@@ -1484,6 +1610,9 @@ def test_restore_data_source_dump_runs_mariadb_without_tls():
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').replace_with(
lambda option, data_source, connection_params=None, restore=False: data_source.get(option)
)
flexmock(module).should_receive('make_defaults_file_options').with_args(
None,
None,
@@ -1528,6 +1657,9 @@ def test_restore_data_source_dump_runs_mariadb_with_username_and_password():
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').replace_with(
lambda option, data_source, connection_params=None, restore=False: data_source.get(option)
)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root',
'trustsome1',
@@ -1575,6 +1707,9 @@ def test_restore_data_source_with_environment_password_transport_skips_defaults_
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').replace_with(
lambda option, data_source, connection_params=None, restore=False: data_source.get(option)
)
flexmock(module).should_receive('make_defaults_file_options').never()
flexmock(module.os).should_receive('environ').and_return(
{'USER': 'root', 'MYSQL_PWD': 'trustsome1'},
@@ -1623,6 +1758,11 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').replace_with(
lambda option, data_source, connection_params=None, restore=False: (
connection_params or {}
).get(option)
)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'cliusername',
'clipassword',
@@ -1687,6 +1827,11 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').replace_with(
lambda option, data_source, connection_params=None, restore=False: data_source.get(
f'restore_{option}'
)
)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'restoreuser',
'restorepass',
@@ -1737,6 +1882,7 @@ def test_restore_data_source_dump_with_dry_run_skips_restore():
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
None,
None,
+150
View File
@@ -65,6 +65,7 @@ def test_database_names_to_dump_queries_mysql_for_database_names():
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
@@ -102,6 +103,7 @@ def test_database_names_to_dump_with_database_name_all_and_skip_names_filters_ou
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options'
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args(
@@ -135,6 +137,46 @@ def test_database_names_to_dump_with_database_name_all_and_skip_names_filters_ou
assert names == ('baz',)
def test_database_names_to_dump_runs_mysql_with_socket_path():
environment = flexmock()
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
(
'mysql',
'--defaults-extra-file=/dev/fd/99',
'--socket',
'/socket',
'--skip-column-names',
'--batch',
'--execute',
'show schemas',
),
environment=environment,
working_directory=None,
).and_return('foo\nbar\nmysql\n').once()
names = module.database_names_to_dump(
{'name': 'all', 'socket_path': '/socket'},
{},
'root',
'trustsome1',
environment,
dry_run=False,
)
assert names == ('foo', 'bar')
def test_database_names_to_dump_with_environment_password_transport_skips_defaults_file_and_passes_user_flag():
environment = flexmock()
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
@@ -143,6 +185,7 @@ def test_database_names_to_dump_with_environment_password_transport_skips_defaul
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).never()
@@ -181,6 +224,7 @@ def test_database_names_to_dump_runs_mysql_with_tls():
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
@@ -219,6 +263,7 @@ def test_database_names_to_dump_runs_mysql_without_tls():
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
@@ -591,6 +636,7 @@ def test_database_names_to_dump_runs_mysql_with_list_options():
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return(('--skip-ssl',), 'my.cnf')
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args('root', 'trustsome1', 'my.cnf').and_return(('--defaults-extra-file=/dev/fd/99',))
@@ -624,6 +670,7 @@ def test_database_names_to_dump_runs_non_default_mysql_with_list_options():
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return(('--skip-ssl',), 'my.cnf')
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args('root', 'trustsome1', 'my.cnf').and_return(('--defaults-extra-file=/dev/fd/99',))
@@ -658,6 +705,7 @@ def test_execute_dump_command_runs_mysqldump():
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
@@ -669,6 +717,7 @@ def test_execute_dump_command_runs_mysqldump():
'mysqldump',
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--single-transaction',
'--databases',
'foo',
'--result-file',
@@ -705,6 +754,7 @@ def test_execute_dump_command_with_environment_password_transport_skips_defaults
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).never()
@@ -715,6 +765,7 @@ def test_execute_dump_command_with_environment_password_transport_skips_defaults
(
'mysqldump',
'--add-drop-database',
'--single-transaction',
'--user',
'root',
'--databases',
@@ -753,6 +804,7 @@ def test_execute_dump_command_runs_mysqldump_without_add_drop_database():
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
@@ -763,6 +815,7 @@ def test_execute_dump_command_runs_mysqldump_without_add_drop_database():
(
'mysqldump',
'--defaults-extra-file=/dev/fd/99',
'--single-transaction',
'--databases',
'foo',
'--result-file',
@@ -799,6 +852,9 @@ def test_execute_dump_command_runs_mysqldump_with_hostname_and_port():
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(
'database.example.org'
)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
@@ -810,6 +866,7 @@ def test_execute_dump_command_runs_mysqldump_with_hostname_and_port():
'mysqldump',
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--single-transaction',
'--host',
'database.example.org',
'--port',
@@ -852,6 +909,7 @@ def test_execute_dump_command_runs_mysqldump_with_tls():
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
@@ -863,6 +921,7 @@ def test_execute_dump_command_runs_mysqldump_with_tls():
'mysqldump',
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--single-transaction',
'--ssl',
'--databases',
'foo',
@@ -900,6 +959,7 @@ def test_execute_dump_command_runs_mysqldump_without_tls():
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
@@ -911,6 +971,7 @@ def test_execute_dump_command_runs_mysqldump_without_tls():
'mysqldump',
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--single-transaction',
'--skip-ssl',
'--databases',
'foo',
@@ -948,6 +1009,7 @@ def test_execute_dump_command_runs_mysqldump_with_username_and_password():
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
@@ -959,6 +1021,7 @@ def test_execute_dump_command_runs_mysqldump_with_username_and_password():
'mysqldump',
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--single-transaction',
'--databases',
'foo',
'--result-file',
@@ -995,6 +1058,7 @@ def test_execute_dump_command_runs_mysqldump_with_options():
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return(('--stuff=such',), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
@@ -1007,6 +1071,7 @@ def test_execute_dump_command_runs_mysqldump_with_options():
'--defaults-extra-file=/dev/fd/99',
'--stuff=such',
'--add-drop-database',
'--single-transaction',
'--databases',
'foo',
'--result-file',
@@ -1043,6 +1108,7 @@ def test_execute_dump_command_runs_non_default_mysqldump():
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
@@ -1054,6 +1120,7 @@ def test_execute_dump_command_runs_non_default_mysqldump():
'custom_mysqldump', # Custom MySQL dump command
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--single-transaction',
'--databases',
'foo',
'--result-file',
@@ -1089,6 +1156,7 @@ def test_execute_dump_command_with_duplicate_dump_skips_mysqldump():
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
@@ -1120,6 +1188,7 @@ def test_execute_dump_command_with_dry_run_skips_mysqldump():
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
@@ -1153,6 +1222,7 @@ def test_restore_data_source_dump_runs_mysql_to_restore():
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args(None, None, None).and_return(())
@@ -1193,6 +1263,7 @@ def test_restore_data_source_dump_runs_mysql_with_options():
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return(('--harder',), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args(None, None, None).and_return(())
@@ -1233,6 +1304,7 @@ def test_restore_data_source_dump_runs_non_default_mysql_with_options():
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return(('--harder',), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args(None, None, None).and_return(())
@@ -1273,6 +1345,9 @@ def test_restore_data_source_dump_runs_mysql_with_hostname_and_port():
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').replace_with(
lambda option, data_source, connection_params=None, restore=False: data_source.get(option)
)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args(None, None, None).and_return(())
@@ -1312,6 +1387,58 @@ def test_restore_data_source_dump_runs_mysql_with_hostname_and_port():
)
def test_restore_data_source_dump_runs_mysql_with_socket_path():
hook_config = [{'name': 'foo', 'socket_path': '/socket'}]
extract_process = flexmock(stdout=flexmock())
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').replace_with(
lambda option, data_source, connection_params=None, restore=False: data_source.get(option)
)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options'
).with_args(
None,
None,
None,
).and_return(())
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_with_processes').with_args(
(
'mysql',
'--batch',
'--socket',
'/socket',
),
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
environment={'USER': 'root'},
working_directory=None,
).once()
module.restore_data_source_dump(
hook_config,
{},
data_source=hook_config[0],
dry_run=False,
extract_process=extract_process,
connection_params={
'hostname': None,
'port': None,
'username': None,
'password': None,
},
borgmatic_runtime_directory='/run/borgmatic',
)
def test_restore_data_source_dump_runs_mysql_with_tls():
hook_config = [{'name': 'foo', 'tls': True}]
extract_process = flexmock(stdout=flexmock())
@@ -1322,6 +1449,9 @@ def test_restore_data_source_dump_runs_mysql_with_tls():
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').replace_with(
lambda option, data_source, connection_params=None, restore=False: data_source.get(option)
)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args(None, None, None).and_return(())
@@ -1363,6 +1493,9 @@ def test_restore_data_source_dump_runs_mysql_without_tls():
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module.database_config).should_receive('resolve_database_option').replace_with(
lambda option, data_source, connection_params=None, restore=False: data_source.get(option)
)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
@@ -1410,6 +1543,9 @@ def test_restore_data_source_dump_runs_mysql_with_username_and_password():
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').replace_with(
lambda option, data_source, connection_params=None, restore=False: data_source.get(option)
)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',))
@@ -1457,6 +1593,9 @@ def test_restore_data_source_with_environment_password_transport_skips_defaults_
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').replace_with(
lambda option, data_source, connection_params=None, restore=False: data_source.get(option)
)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).never()
@@ -1509,6 +1648,11 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').replace_with(
lambda option, data_source, connection_params=None, restore=False: (
connection_params or {}
).get(option)
)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args('cliusername', 'clipassword', None).and_return(
@@ -1575,6 +1719,11 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').replace_with(
lambda option, data_source, connection_params=None, restore=False: data_source.get(
f'restore_{option}'
)
)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args('restoreuser', 'restorepass', None).and_return(
@@ -1627,6 +1776,7 @@ def test_restore_data_source_dump_with_dry_run_skips_restore():
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'make_defaults_file_options',
).with_args(None, None, None).and_return(())
-18
View File
@@ -328,9 +328,6 @@ def test_dump_data_sources_snapshots_and_mounts_and_replaces_patterns():
'/run/borgmatic',
).and_return(Pattern('/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir'))
patterns = [Pattern('/mnt/dataset/subdir')]
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).and_return(0)
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
Pattern('/mnt/dataset/subdir'),
@@ -338,7 +335,6 @@ def test_dump_data_sources_snapshots_and_mounts_and_replaces_patterns():
'/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
assert (
@@ -359,9 +355,6 @@ def test_dump_data_sources_with_no_datasets_skips_snapshots():
flexmock(module.os).should_receive('getpid').and_return(1234)
flexmock(module).should_receive('snapshot_dataset').never()
flexmock(module).should_receive('mount_snapshot').never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
patterns = [Pattern('/mnt/dataset')]
@@ -407,9 +400,6 @@ def test_dump_data_sources_uses_custom_commands():
dataset,
'/run/borgmatic',
).and_return(Pattern('/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir'))
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).and_return(0)
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
Pattern('/mnt/dataset/subdir'),
@@ -417,7 +407,6 @@ def test_dump_data_sources_uses_custom_commands():
'/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
patterns = [Pattern('/mnt/dataset/subdir')]
hook_config = {
@@ -448,9 +437,6 @@ def test_dump_data_sources_with_dry_run_skips_commands_and_does_not_touch_patter
flexmock(module.os).should_receive('getpid').and_return(1234)
flexmock(module).should_receive('snapshot_dataset').never()
flexmock(module).should_receive('mount_snapshot').never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).and_return(0)
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
patterns = [Pattern('/mnt/dataset')]
@@ -494,9 +480,6 @@ def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained
dataset,
'/run/borgmatic',
).and_return(Pattern('/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir'))
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).and_return(0)
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
Pattern('/mnt/dataset/subdir'),
@@ -504,7 +487,6 @@ def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained
'/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
patterns = [Pattern('/hmm')]