mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-30 21:33:00 +02:00
Merge branch 'main' into subprocess-stderr-to-error-logs
This commit is contained in:
@@ -4,6 +4,13 @@
|
||||
* #858: With the "--log-json" flag, log borgmatic's own logs as JSON, not just Borg's.
|
||||
* #1204: When verbosity levels differ between console/monitoring/syslog/file, log Borg's output to
|
||||
each one at a different level.
|
||||
* #1208: Fix for the "restore" action incorrectly extracting more database dumps than the
|
||||
"--database" flag specifies.
|
||||
* #1210: Fix an error when running the "spot" check or "extract" action with the "progress" option
|
||||
or "--progress" flag.
|
||||
* #1211: Fix an error about the runtime directory getting excluded by tweaking its logic and
|
||||
lowering the error to a warning.
|
||||
* #1212: Fix an error when restoring multiple directory-format database dumps at once.
|
||||
* When syslog verbosity is enabled, log to systemd's journal (if present) with structured data.
|
||||
|
||||
2.0.13
|
||||
|
||||
@@ -380,10 +380,7 @@ def collect_spot_check_source_paths(
|
||||
dry_run=True,
|
||||
repository_path=repository['path'],
|
||||
# Omit "progress" because it interferes with "list_details".
|
||||
config=dict(
|
||||
{option: value for option, value in config.items() if option != 'progress'},
|
||||
list_details=True,
|
||||
),
|
||||
config=dict(config, progress=False, list_details=True),
|
||||
patterns=borgmatic.actions.pattern.process_patterns(
|
||||
borgmatic.actions.pattern.collect_patterns(config)
|
||||
+ tuple(
|
||||
|
||||
@@ -127,8 +127,6 @@ def run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version):
|
||||
bootstrap_arguments.repository,
|
||||
archive_name,
|
||||
[config_path.lstrip(os.path.sep) for config_path in manifest_config_paths],
|
||||
# Only add progress here and not the extract_archive() call above, because progress
|
||||
# conflicts with extract_to_stdout.
|
||||
dict(config, progress=bootstrap_arguments.progress or False),
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
|
||||
@@ -129,14 +129,14 @@ def get_configured_data_source(config, restore_dump):
|
||||
|
||||
|
||||
def strip_path_prefix_from_extracted_dump_destination(
|
||||
destination_path,
|
||||
extract_path,
|
||||
borgmatic_runtime_directory,
|
||||
):
|
||||
'''
|
||||
Directory-format dump files get extracted into a temporary directory containing a path prefix
|
||||
that depends how the files were stored in the archive. So, given the destination path where the
|
||||
dump was extracted and the borgmatic runtime directory, move the dump files such that the
|
||||
restore doesn't have to deal with that varying path prefix.
|
||||
that depends how the files were stored in the archive. So, given the path where the dump was
|
||||
extracted and the borgmatic runtime directory, move the dump files such that the restore doesn't
|
||||
have to deal with that varying path prefix.
|
||||
|
||||
For instance, if the dump was extracted to:
|
||||
|
||||
@@ -150,16 +150,16 @@ def strip_path_prefix_from_extracted_dump_destination(
|
||||
|
||||
/run/user/0/borgmatic/postgresql_databases/test/...
|
||||
'''
|
||||
for subdirectory_path, _, _ in os.walk(destination_path):
|
||||
for subdirectory_path, _, _ in os.walk(extract_path):
|
||||
databases_directory = os.path.basename(subdirectory_path)
|
||||
|
||||
if not databases_directory.endswith('_databases'):
|
||||
continue
|
||||
|
||||
shutil.move(
|
||||
subdirectory_path,
|
||||
os.path.join(borgmatic_runtime_directory, databases_directory),
|
||||
)
|
||||
destination_path = os.path.join(borgmatic_runtime_directory, databases_directory)
|
||||
shutil.rmtree(destination_path, ignore_errors=True)
|
||||
shutil.move(subdirectory_path, destination_path)
|
||||
|
||||
break
|
||||
|
||||
|
||||
@@ -200,6 +200,10 @@ def restore_single_dump(
|
||||
borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
|
||||
borgmatic_runtime_directory,
|
||||
data_source['name'],
|
||||
data_source.get('hostname'),
|
||||
data_source.get('port'),
|
||||
data_source.get('container'),
|
||||
data_source.get('label'),
|
||||
)[hook_name.split('_databases', 1)[0]]
|
||||
|
||||
destination_path = (
|
||||
@@ -267,9 +271,9 @@ def collect_dumps_from_archive(
|
||||
Given a local or remote repository path, a resolved archive name, a configuration dict, the
|
||||
local Borg version, global arguments an argparse.Namespace, local and remote Borg paths, and the
|
||||
borgmatic runtime directory, query the archive for the names of data sources dumps it contains
|
||||
and return them as a set of Dump instances.
|
||||
and return them as a tuple of Dump instances.
|
||||
'''
|
||||
dumps_from_archive = set()
|
||||
dumps_from_archive = {} # Use a dict as an ordered set.
|
||||
|
||||
# There is (at most) one dump metadata file per data source hook. Load each.
|
||||
for dumps_metadata_path in borgmatic.borg.list.capture_archive_listing(
|
||||
@@ -298,33 +302,30 @@ def collect_dumps_from_archive(
|
||||
if not dumps_metadata_path:
|
||||
continue
|
||||
|
||||
dumps_from_archive.update(
|
||||
set(
|
||||
borgmatic.hooks.data_source.dump.parse_data_source_dumps_metadata(
|
||||
borgmatic.borg.extract.extract_archive(
|
||||
global_arguments.dry_run,
|
||||
repository,
|
||||
archive,
|
||||
[dumps_metadata_path],
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
extract_to_stdout=True,
|
||||
)
|
||||
.stdout.read()
|
||||
.decode(),
|
||||
dumps_metadata_path,
|
||||
)
|
||||
for dump in borgmatic.hooks.data_source.dump.parse_data_source_dumps_metadata(
|
||||
borgmatic.borg.extract.extract_archive(
|
||||
global_arguments.dry_run,
|
||||
repository,
|
||||
archive,
|
||||
[dumps_metadata_path],
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
extract_to_stdout=True,
|
||||
)
|
||||
)
|
||||
.stdout.read()
|
||||
.decode(),
|
||||
dumps_metadata_path,
|
||||
):
|
||||
dumps_from_archive[dump] = None
|
||||
|
||||
# If we've successfully loaded any dumps metadata, we're done.
|
||||
if dumps_from_archive:
|
||||
logger.debug('Collecting database dumps from archive data source dumps metadata files')
|
||||
|
||||
return dumps_from_archive
|
||||
return tuple(dumps_from_archive.keys())
|
||||
|
||||
# No dumps metadata files were found, so for backwards compatibility, fall back to parsing the
|
||||
# paths of dumps found in the archive to get their respective dump metadata.
|
||||
@@ -388,11 +389,11 @@ def collect_dumps_from_archive(
|
||||
except (ValueError, TypeError):
|
||||
port = None
|
||||
|
||||
dumps_from_archive.add(
|
||||
dumps_from_archive[
|
||||
Dump(
|
||||
hook_name, data_source_name, None if hostname == 'localhost' else hostname, port
|
||||
)
|
||||
)
|
||||
] = None
|
||||
|
||||
# We've successfully parsed the dump path, so need to probe any further.
|
||||
break
|
||||
@@ -401,20 +402,21 @@ def collect_dumps_from_archive(
|
||||
f'Ignoring invalid data source dump path "{dump_path}" in archive {archive}',
|
||||
)
|
||||
|
||||
return dumps_from_archive
|
||||
return tuple(dumps_from_archive.keys())
|
||||
|
||||
|
||||
def get_dumps_to_restore(restore_arguments, dumps_from_archive):
|
||||
'''
|
||||
Given restore arguments as an argparse.Namespace instance indicating which dumps to restore and
|
||||
a set of Dump instances representing the dumps found in an archive, return a set of specific
|
||||
a tuple of Dump instances representing the dumps found in an archive, return a tuple of specific
|
||||
Dump instances from the archive to restore. As part of this, replace any Dump having a data
|
||||
source name of "all" with multiple named Dump instances as appropriate.
|
||||
|
||||
Raise ValueError if any of the requested data source names cannot be found in the archive or if
|
||||
there are multiple archive dump matches for a given requested dump.
|
||||
'''
|
||||
requested_dumps = (
|
||||
requested_dumps = tuple(
|
||||
# Use a dict comprehension as an ordered set.
|
||||
{
|
||||
Dump(
|
||||
hook_name=(
|
||||
@@ -431,16 +433,16 @@ def get_dumps_to_restore(restore_arguments, dumps_from_archive):
|
||||
port=restore_arguments.original_port,
|
||||
label=restore_arguments.original_label or UNSPECIFIED,
|
||||
container=restore_arguments.original_container or UNSPECIFIED,
|
||||
)
|
||||
): None
|
||||
for name in restore_arguments.data_sources or (UNSPECIFIED,)
|
||||
}
|
||||
}.keys()
|
||||
if restore_arguments.hook
|
||||
or restore_arguments.data_sources
|
||||
or restore_arguments.original_hostname
|
||||
or restore_arguments.original_port
|
||||
or restore_arguments.original_label
|
||||
or restore_arguments.original_container
|
||||
else {
|
||||
else (
|
||||
Dump(
|
||||
hook_name=UNSPECIFIED,
|
||||
data_source_name='all',
|
||||
@@ -449,14 +451,15 @@ def get_dumps_to_restore(restore_arguments, dumps_from_archive):
|
||||
label=UNSPECIFIED,
|
||||
container=UNSPECIFIED,
|
||||
),
|
||||
}
|
||||
)
|
||||
)
|
||||
missing_dumps = set()
|
||||
dumps_to_restore = set()
|
||||
dumps_to_restore = {} # Use a dict as an ordered set.
|
||||
|
||||
# If there's a requested "all" dump, add every dump from the archive to the dumps to restore.
|
||||
if any(dump for dump in requested_dumps if dump.data_source_name == 'all'):
|
||||
dumps_to_restore.update(dumps_from_archive)
|
||||
for dump in dumps_from_archive:
|
||||
dumps_to_restore[dump] = None
|
||||
|
||||
# If any archive dump matches a requested dump, add the archive dump to the dumps to restore.
|
||||
for requested_dump in requested_dumps:
|
||||
@@ -472,7 +475,7 @@ def get_dumps_to_restore(restore_arguments, dumps_from_archive):
|
||||
if len(matching_dumps) == 0:
|
||||
missing_dumps.add(requested_dump)
|
||||
elif len(matching_dumps) == 1:
|
||||
dumps_to_restore.add(matching_dumps[0])
|
||||
dumps_to_restore[matching_dumps[0]] = None
|
||||
else:
|
||||
raise ValueError(
|
||||
f'Cannot restore data source {render_dump_metadata(requested_dump)} because there are multiple matching dumps in the archive. Try adding flags to disambiguate.',
|
||||
@@ -487,20 +490,20 @@ def get_dumps_to_restore(restore_arguments, dumps_from_archive):
|
||||
f"Cannot restore data source dump{'s' if len(missing_dumps) > 1 else ''} {rendered_dumps} missing from archive",
|
||||
)
|
||||
|
||||
return dumps_to_restore
|
||||
return tuple(dumps_to_restore.keys())
|
||||
|
||||
|
||||
def ensure_requested_dumps_restored(dumps_to_restore, dumps_actually_restored):
|
||||
'''
|
||||
Given a set of requested dumps to restore and a set of dumps actually restored, raise ValueError
|
||||
if any requested dumps to restore weren't restored, indicating that they were missing from the
|
||||
configuration.
|
||||
Given a tuple of requested dumps to restore and a set of dumps actually restored, raise
|
||||
ValueError if any requested dumps to restore weren't restored, indicating that they were missing
|
||||
from the configuration.
|
||||
'''
|
||||
if not dumps_actually_restored:
|
||||
if not dumps_to_restore:
|
||||
raise ValueError('No data source dumps were found to restore')
|
||||
|
||||
missing_dumps = sorted(
|
||||
dumps_to_restore - dumps_actually_restored,
|
||||
set(dumps_to_restore) - dumps_actually_restored,
|
||||
key=lambda dump: dump.data_source_name,
|
||||
)
|
||||
|
||||
|
||||
@@ -96,22 +96,36 @@ def validate_planned_backup_paths(
|
||||
if path_line and path_line.startswith(('- ', '+ '))
|
||||
)
|
||||
|
||||
include_pattern_paths = {
|
||||
pattern.path
|
||||
for pattern in patterns
|
||||
if pattern.type == borgmatic.borg.pattern.Pattern_type.INCLUDE
|
||||
}
|
||||
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
|
||||
# Skip root patterns that have corresponding include patterns, because those will "punch
|
||||
# through" any subsequent excludes.
|
||||
if pattern.path not in include_pattern_paths
|
||||
)
|
||||
|
||||
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.',
|
||||
)
|
||||
# If all root patterns in the runtime directory are missing from the paths Borg is planning to
|
||||
# backup, then they must've gotten excluded, e.g. by user-configured excludes. Warn accordingly.
|
||||
if (
|
||||
not dry_run
|
||||
and os.path.exists(borgmatic_runtime_directory)
|
||||
and runtime_directory_root_patterns
|
||||
and not any(
|
||||
any_parent_directories(path, (pattern.path,))
|
||||
for pattern in runtime_directory_root_patterns
|
||||
for path in paths
|
||||
)
|
||||
):
|
||||
logger.warning(
|
||||
f'The runtime directory {os.path.normpath(borgmatic_runtime_directory)} overlaps with the configured excludes (or the snapshotted source directories are empty). Please ensure the runtime directory is not excluded.'
|
||||
)
|
||||
|
||||
# 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
|
||||
|
||||
+12
-15
@@ -97,9 +97,6 @@ def extract_archive(
|
||||
lock_wait = config.get('lock_wait', None)
|
||||
extra_borg_options = config.get('extra_borg_options', {}).get('extract', '')
|
||||
|
||||
if config.get('progress') and extract_to_stdout:
|
||||
raise ValueError('progress and extract to stdout cannot both be set')
|
||||
|
||||
if feature.available(feature.Feature.NUMERIC_IDS, local_borg_version):
|
||||
numeric_ids_flags = ('--numeric-ids',) if config.get('numeric_ids') else ()
|
||||
else:
|
||||
@@ -133,7 +130,7 @@ def extract_archive(
|
||||
+ (('--debug', '--list', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ (('--dry-run',) if dry_run else ())
|
||||
+ (('--strip-components', str(strip_components)) if strip_components else ())
|
||||
+ (('--progress',) if config.get('progress') else ())
|
||||
+ (('--progress',) if config.get('progress') and not extract_to_stdout else ())
|
||||
+ (('--stdout',) if extract_to_stdout else ())
|
||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
||||
+ flags.make_repository_archive_flags(
|
||||
@@ -152,6 +149,17 @@ def extract_archive(
|
||||
os.path.join(working_directory or '', destination_path) if destination_path else None
|
||||
)
|
||||
|
||||
if extract_to_stdout:
|
||||
return execute_command(
|
||||
full_command,
|
||||
output_file=subprocess.PIPE,
|
||||
run_to_completion=False,
|
||||
environment=environment.make_environment(config),
|
||||
working_directory=full_destination_path,
|
||||
borg_local_path=local_path,
|
||||
borg_exit_codes=borg_exit_codes,
|
||||
)
|
||||
|
||||
# The progress output isn't compatible with captured and logged output, as progress messes with
|
||||
# the terminal directly.
|
||||
if config.get('progress'):
|
||||
@@ -165,17 +173,6 @@ def extract_archive(
|
||||
)
|
||||
return None
|
||||
|
||||
if extract_to_stdout:
|
||||
return execute_command(
|
||||
full_command,
|
||||
output_file=subprocess.PIPE,
|
||||
run_to_completion=False,
|
||||
environment=environment.make_environment(config),
|
||||
working_directory=full_destination_path,
|
||||
borg_local_path=local_path,
|
||||
borg_exit_codes=borg_exit_codes,
|
||||
)
|
||||
|
||||
# Don't give Borg local path so as to error on warnings, as "borg extract" only gives a warning
|
||||
# if the restore paths don't exist in the archive.
|
||||
execute_command(
|
||||
|
||||
@@ -116,6 +116,10 @@ def make_data_source_dump_patterns(
|
||||
config,
|
||||
borgmatic_runtime_directory,
|
||||
name=None,
|
||||
hostname=None,
|
||||
port=None,
|
||||
container=None,
|
||||
label=None,
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Restores are implemented via the separate, purpose-specific "bootstrap" action rather than the
|
||||
|
||||
@@ -412,6 +412,10 @@ def make_data_source_dump_patterns(
|
||||
config,
|
||||
borgmatic_runtime_directory,
|
||||
name=None,
|
||||
hostname=None,
|
||||
port=None,
|
||||
container=None,
|
||||
label=None,
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Restores aren't implemented, because stored files can be extracted directly with "extract".
|
||||
|
||||
@@ -2,6 +2,7 @@ import fnmatch
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
|
||||
import borgmatic.actions.restore
|
||||
@@ -114,16 +115,33 @@ def remove_data_source_dumps(dump_path, data_source_type_name, dry_run):
|
||||
shutil.rmtree(dump_path)
|
||||
|
||||
|
||||
END_OF_STRING_PATTTERN = re.compile(r'\\z', flags=re.IGNORECASE)
|
||||
|
||||
|
||||
def convert_glob_patterns_to_borg_pattern(patterns):
|
||||
'''
|
||||
Convert a sequence of shell glob patterns like "/etc/*", "/tmp/*" to the corresponding Borg
|
||||
regular expression archive pattern as a single string like "re:etc/.*|tmp/.*".
|
||||
'''
|
||||
# Remove the "\z" or "\Z" generated by fnmatch.translate() because we don't want the pattern to
|
||||
# match only at the end of a path, as directory format dumps require extracting files with paths
|
||||
# longer than the pattern. E.g., a pattern of "borgmatic/*/foo_databases/test" should also match
|
||||
# paths like "borgmatic/*/foo_databases/test/toc.dat"
|
||||
# This deserves some explanation. Here's what this code is doing for each shell glob pattern:
|
||||
#
|
||||
# * Strip off the leading slash, since Borg doesn't store leading slashes in archives.
|
||||
# * Use fnmatch.translate() to convert the pattern to a Borg pattern.
|
||||
# * Remove the "\z" or \"Z" end-of-string special character generated by fnmatch.translate(),
|
||||
# because Borg doesn't like it. Replace it with a "$" end-of-string special character
|
||||
# instead. And yes, this is using a regular expression to modify a regular expression.
|
||||
# * Do the above for each of:
|
||||
# * The plain pattern. This supports the use case of a standard database where its name is a
|
||||
# filename in the path. Example: borgmatic/foo_databases/hostname/test
|
||||
# * The pattern with "/*" tacked onto the end of it. This supports the use case of a
|
||||
# directory-format database where its name is a directory name in the path. Example:
|
||||
# borgmatic/foo_databases/hostname/test/toc.dat
|
||||
#
|
||||
# Join the resulting transformed patterns together with "|" and return them as a string.
|
||||
return 're:' + '|'.join(
|
||||
fnmatch.translate(pattern.lstrip('/')).replace('\\z', '').replace('\\Z', '')
|
||||
re.sub(END_OF_STRING_PATTTERN, '$', fnmatch.translate(stripped))
|
||||
+ '|'
|
||||
+ re.sub(END_OF_STRING_PATTTERN, '$', fnmatch.translate(stripped + '/*'))
|
||||
for pattern in patterns
|
||||
for stripped in (pattern.lstrip('/'),)
|
||||
)
|
||||
|
||||
@@ -452,6 +452,10 @@ def make_data_source_dump_patterns(
|
||||
config,
|
||||
borgmatic_runtime_directory,
|
||||
name=None,
|
||||
hostname=None,
|
||||
port=None,
|
||||
container=None,
|
||||
label=None,
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Restores aren't implemented, because stored files can be extracted directly with "extract".
|
||||
|
||||
@@ -410,6 +410,10 @@ def make_data_source_dump_patterns(
|
||||
config,
|
||||
borgmatic_runtime_directory,
|
||||
name=None,
|
||||
hostname=None,
|
||||
port=None,
|
||||
container=None,
|
||||
label=None,
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Given a sequence of configurations dicts, a configuration dict, the borgmatic runtime directory,
|
||||
@@ -419,16 +423,24 @@ def make_data_source_dump_patterns(
|
||||
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
||||
|
||||
return (
|
||||
dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, label='*'),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path('borgmatic'), name, hostname, port, container, label
|
||||
),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_runtime_directory),
|
||||
name,
|
||||
label='*',
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_source_directory),
|
||||
name,
|
||||
label='*',
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -212,6 +212,10 @@ def make_data_source_dump_patterns(
|
||||
config,
|
||||
borgmatic_runtime_directory,
|
||||
name=None,
|
||||
hostname=None,
|
||||
port=None,
|
||||
container=None,
|
||||
label=None,
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Given a sequence of configurations dicts, a configuration dict, the borgmatic runtime directory,
|
||||
@@ -221,16 +225,24 @@ def make_data_source_dump_patterns(
|
||||
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
||||
|
||||
return (
|
||||
dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, label='*'),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path('borgmatic'), name, hostname, port, container, label
|
||||
),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_runtime_directory),
|
||||
name,
|
||||
label='*',
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_source_directory),
|
||||
name,
|
||||
label='*',
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -341,6 +341,10 @@ def make_data_source_dump_patterns(
|
||||
config,
|
||||
borgmatic_runtime_directory,
|
||||
name=None,
|
||||
hostname=None,
|
||||
port=None,
|
||||
container=None,
|
||||
label=None,
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Given a sequence of configurations dicts, a configuration dict, the borgmatic runtime directory,
|
||||
@@ -350,16 +354,24 @@ def make_data_source_dump_patterns(
|
||||
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
||||
|
||||
return (
|
||||
dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, label='*'),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path('borgmatic'), name, hostname, port, container, label
|
||||
),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_runtime_directory),
|
||||
name,
|
||||
label='*',
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_source_directory),
|
||||
name,
|
||||
label='*',
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -302,6 +302,10 @@ def make_data_source_dump_patterns(
|
||||
config,
|
||||
borgmatic_runtime_directory,
|
||||
name=None,
|
||||
hostname=None,
|
||||
port=None,
|
||||
container=None,
|
||||
label=None,
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Given a sequence of configurations dicts, a configuration dict, the borgmatic runtime directory,
|
||||
@@ -311,16 +315,24 @@ def make_data_source_dump_patterns(
|
||||
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
||||
|
||||
return (
|
||||
dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, label='*'),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path('borgmatic'), name, hostname, port, container, label
|
||||
),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_runtime_directory),
|
||||
name,
|
||||
label='*',
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_source_directory),
|
||||
name,
|
||||
label='*',
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -144,6 +144,10 @@ def make_data_source_dump_patterns(
|
||||
config,
|
||||
borgmatic_runtime_directory,
|
||||
name=None,
|
||||
hostname=None,
|
||||
port=None,
|
||||
container=None,
|
||||
label=None,
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Given a sequence of configurations dicts, a configuration dict, the borgmatic runtime directory,
|
||||
@@ -153,16 +157,24 @@ def make_data_source_dump_patterns(
|
||||
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
||||
|
||||
return (
|
||||
dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, label='*'),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path('borgmatic'), name, hostname, port, container, label
|
||||
),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_runtime_directory),
|
||||
name,
|
||||
label='*',
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_source_directory),
|
||||
name,
|
||||
label='*',
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -453,6 +453,10 @@ def make_data_source_dump_patterns(
|
||||
config,
|
||||
borgmatic_runtime_directory,
|
||||
name=None,
|
||||
hostname=None,
|
||||
port=None,
|
||||
container=None,
|
||||
label=None,
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Restores aren't implemented, because stored files can be extracted directly with "extract".
|
||||
|
||||
@@ -651,7 +651,7 @@ def test_collect_spot_check_source_paths_omits_progress_from_create_dry_run_comm
|
||||
flexmock(module.borgmatic.borg.create).should_receive('make_base_create_command').with_args(
|
||||
dry_run=True,
|
||||
repository_path='repo',
|
||||
config={'working_directory': '/', 'list_details': True},
|
||||
config={'working_directory': '/', 'progress': False, 'list_details': True},
|
||||
patterns=[Pattern('foo'), Pattern('bar')],
|
||||
local_borg_version=object,
|
||||
global_arguments=object,
|
||||
@@ -941,7 +941,7 @@ def test_collect_spot_check_source_paths_uses_working_directory():
|
||||
flexmock(module.borgmatic.borg.create).should_receive('make_base_create_command').with_args(
|
||||
dry_run=True,
|
||||
repository_path='repo',
|
||||
config={'working_directory': '/working/dir', 'list_details': True},
|
||||
config={'working_directory': '/working/dir', 'progress': False, 'list_details': True},
|
||||
patterns=[Pattern('foo'), Pattern('bar')],
|
||||
local_borg_version=object,
|
||||
global_arguments=object,
|
||||
|
||||
@@ -295,6 +295,7 @@ def test_strip_path_prefix_from_extracted_dump_destination_renames_first_matchin
|
||||
],
|
||||
)
|
||||
|
||||
flexmock(module.shutil).should_receive('rmtree')
|
||||
flexmock(module.shutil).should_receive('move').with_args(
|
||||
'/foo/bar/postgresql_databases',
|
||||
'/run/user/0/borgmatic/postgresql_databases',
|
||||
@@ -315,6 +316,10 @@ def test_restore_single_dump_extracts_and_restores_single_file_dump():
|
||||
object,
|
||||
object,
|
||||
object,
|
||||
object,
|
||||
object,
|
||||
object,
|
||||
object,
|
||||
).and_return({'postgresql': flexmock()})
|
||||
flexmock(module.tempfile).should_receive('mkdtemp').never()
|
||||
flexmock(module.borgmatic.hooks.data_source.dump).should_receive(
|
||||
@@ -359,6 +364,10 @@ def test_restore_single_dump_extracts_and_restores_directory_dump():
|
||||
object,
|
||||
object,
|
||||
object,
|
||||
object,
|
||||
object,
|
||||
object,
|
||||
object,
|
||||
).and_return({'postgresql': flexmock()})
|
||||
flexmock(module.tempfile).should_receive('mkdtemp').once().and_return(
|
||||
'/run/user/0/borgmatic/tmp1234',
|
||||
@@ -405,6 +414,10 @@ def test_restore_single_dump_with_directory_dump_error_cleans_up_temporary_direc
|
||||
object,
|
||||
object,
|
||||
object,
|
||||
object,
|
||||
object,
|
||||
object,
|
||||
object,
|
||||
).and_return({'postgresql': flexmock()})
|
||||
flexmock(module.tempfile).should_receive('mkdtemp').once().and_return(
|
||||
'/run/user/0/borgmatic/tmp1234',
|
||||
@@ -452,6 +465,10 @@ def test_restore_single_dump_with_directory_dump_and_dry_run_skips_directory_mov
|
||||
object,
|
||||
object,
|
||||
object,
|
||||
object,
|
||||
object,
|
||||
object,
|
||||
object,
|
||||
).and_return({'postgresql': flexmock()})
|
||||
flexmock(module.tempfile).should_receive('mkdtemp').once().and_return('/run/borgmatic/tmp1234')
|
||||
flexmock(module.borgmatic.hooks.data_source.dump).should_receive(
|
||||
@@ -525,7 +542,7 @@ def test_collect_dumps_from_archive_with_dumps_metadata_parses_it():
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
assert archive_dumps == set(dumps_metadata)
|
||||
assert archive_dumps == tuple(dumps_metadata)
|
||||
|
||||
|
||||
def test_collect_dumps_from_archive_with_empty_dumps_metadata_path_falls_back_to_parsing_archive_paths():
|
||||
@@ -563,11 +580,11 @@ def test_collect_dumps_from_archive_with_empty_dumps_metadata_path_falls_back_to
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
assert archive_dumps == {
|
||||
assert archive_dumps == (
|
||||
module.Dump('postgresql_databases', 'foo'),
|
||||
module.Dump('postgresql_databases', 'bar', 'host', 1234),
|
||||
module.Dump('mysql_databases', 'quux'),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_collect_dumps_from_archive_without_dumps_metadata_falls_back_to_parsing_archive_paths():
|
||||
@@ -605,11 +622,11 @@ def test_collect_dumps_from_archive_without_dumps_metadata_falls_back_to_parsing
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
assert archive_dumps == {
|
||||
assert archive_dumps == (
|
||||
module.Dump('postgresql_databases', 'foo'),
|
||||
module.Dump('postgresql_databases', 'bar', 'host', 1234),
|
||||
module.Dump('mysql_databases', 'quux'),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_collect_dumps_from_archive_parses_archive_paths_with_different_base_directories():
|
||||
@@ -648,12 +665,12 @@ def test_collect_dumps_from_archive_parses_archive_paths_with_different_base_dir
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
assert archive_dumps == {
|
||||
assert archive_dumps == (
|
||||
module.Dump('postgresql_databases', 'foo'),
|
||||
module.Dump('postgresql_databases', 'bar'),
|
||||
module.Dump('postgresql_databases', 'baz'),
|
||||
module.Dump('mysql_databases', 'quux'),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_collect_dumps_from_archive_parses_directory_format_archive_paths():
|
||||
@@ -690,9 +707,7 @@ def test_collect_dumps_from_archive_parses_directory_format_archive_paths():
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
assert archive_dumps == {
|
||||
module.Dump('postgresql_databases', 'foo'),
|
||||
}
|
||||
assert archive_dumps == (module.Dump('postgresql_databases', 'foo'),)
|
||||
|
||||
|
||||
def test_collect_dumps_from_archive_skips_bad_archive_paths_or_bad_path_components():
|
||||
@@ -732,18 +747,18 @@ def test_collect_dumps_from_archive_skips_bad_archive_paths_or_bad_path_componen
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
assert archive_dumps == {
|
||||
assert archive_dumps == (
|
||||
module.Dump('postgresql_databases', 'foo'),
|
||||
module.Dump('postgresql_databases', 'bar'),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_get_dumps_to_restore_gets_requested_dumps_found_in_archive():
|
||||
dumps_from_archive = {
|
||||
dumps_from_archive = (
|
||||
module.Dump('postgresql_databases', 'foo'),
|
||||
module.Dump('postgresql_databases', 'bar'),
|
||||
module.Dump('postgresql_databases', 'baz'),
|
||||
}
|
||||
)
|
||||
flexmock(module).should_receive('dumps_match').and_return(False)
|
||||
flexmock(module).should_receive('dumps_match').with_args(
|
||||
module.Dump(
|
||||
@@ -776,16 +791,14 @@ def test_get_dumps_to_restore_gets_requested_dumps_found_in_archive():
|
||||
original_container=None,
|
||||
),
|
||||
dumps_from_archive=dumps_from_archive,
|
||||
) == {
|
||||
) == (
|
||||
module.Dump('postgresql_databases', 'foo'),
|
||||
module.Dump('postgresql_databases', 'bar'),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_get_dumps_to_restore_raises_for_requested_dumps_missing_from_archive():
|
||||
dumps_from_archive = {
|
||||
module.Dump('postgresql_databases', 'foo'),
|
||||
}
|
||||
dumps_from_archive = (module.Dump('postgresql_databases', 'foo'),)
|
||||
flexmock(module).should_receive('dumps_match').and_return(False)
|
||||
flexmock(module).should_receive('dumps_match').with_args(
|
||||
module.Dump(
|
||||
@@ -815,10 +828,10 @@ def test_get_dumps_to_restore_raises_for_requested_dumps_missing_from_archive():
|
||||
|
||||
|
||||
def test_get_dumps_to_restore_without_requested_dumps_finds_all_archive_dumps():
|
||||
dumps_from_archive = {
|
||||
dumps_from_archive = (
|
||||
module.Dump('postgresql_databases', 'foo'),
|
||||
module.Dump('postgresql_databases', 'bar'),
|
||||
}
|
||||
)
|
||||
flexmock(module).should_receive('dumps_match').and_return(False)
|
||||
|
||||
assert (
|
||||
@@ -838,10 +851,10 @@ def test_get_dumps_to_restore_without_requested_dumps_finds_all_archive_dumps():
|
||||
|
||||
|
||||
def test_get_dumps_to_restore_with_all_in_requested_dumps_finds_all_archive_dumps():
|
||||
dumps_from_archive = {
|
||||
dumps_from_archive = (
|
||||
module.Dump('postgresql_databases', 'foo'),
|
||||
module.Dump('postgresql_databases', 'bar'),
|
||||
}
|
||||
)
|
||||
flexmock(module).should_receive('dumps_match').and_return(False)
|
||||
flexmock(module).should_receive('dumps_match').with_args(
|
||||
module.Dump(
|
||||
@@ -881,10 +894,10 @@ def test_get_dumps_to_restore_with_all_in_requested_dumps_finds_all_archive_dump
|
||||
|
||||
|
||||
def test_get_dumps_to_restore_with_all_in_requested_dumps_plus_additional_requested_dumps_omits_duplicates():
|
||||
dumps_from_archive = {
|
||||
dumps_from_archive = (
|
||||
module.Dump('postgresql_databases', 'foo'),
|
||||
module.Dump('postgresql_databases', 'bar'),
|
||||
}
|
||||
)
|
||||
flexmock(module).should_receive('dumps_match').and_return(False)
|
||||
flexmock(module).should_receive('dumps_match').with_args(
|
||||
module.Dump(
|
||||
@@ -957,10 +970,10 @@ def test_get_dumps_to_restore_raises_for_multiple_matching_dumps_in_archive():
|
||||
original_label=None,
|
||||
original_container=None,
|
||||
),
|
||||
dumps_from_archive={
|
||||
dumps_from_archive=(
|
||||
module.Dump('postgresql_databases', 'foo'),
|
||||
module.Dump('mariadb_databases', 'foo'),
|
||||
},
|
||||
),
|
||||
)
|
||||
assert 'Try adding flags to disambiguate.' in str(exc_info.value)
|
||||
|
||||
@@ -989,17 +1002,17 @@ def test_get_dumps_to_restore_raises_for_all_in_requested_dumps_and_requested_du
|
||||
original_label=None,
|
||||
original_container=None,
|
||||
),
|
||||
dumps_from_archive={module.Dump('postgresql_databases', 'foo')},
|
||||
dumps_from_archive=(module.Dump('postgresql_databases', 'foo'),),
|
||||
)
|
||||
assert 'dump test missing from archive' in str(exc_info.value)
|
||||
|
||||
|
||||
def test_get_dumps_to_restore_with_requested_hook_name_filters_dumps_found_in_archive():
|
||||
dumps_from_archive = {
|
||||
dumps_from_archive = (
|
||||
module.Dump('mariadb_databases', 'foo'),
|
||||
module.Dump('postgresql_databases', 'foo'),
|
||||
module.Dump('sqlite_databases', 'bar'),
|
||||
}
|
||||
)
|
||||
flexmock(module).should_receive('dumps_match').and_return(False)
|
||||
flexmock(module).should_receive('dumps_match').with_args(
|
||||
module.Dump(
|
||||
@@ -1022,17 +1035,15 @@ def test_get_dumps_to_restore_with_requested_hook_name_filters_dumps_found_in_ar
|
||||
original_container=None,
|
||||
),
|
||||
dumps_from_archive=dumps_from_archive,
|
||||
) == {
|
||||
module.Dump('postgresql_databases', 'foo'),
|
||||
}
|
||||
) == (module.Dump('postgresql_databases', 'foo'),)
|
||||
|
||||
|
||||
def test_get_dumps_to_restore_with_requested_shortened_hook_name_filters_dumps_found_in_archive():
|
||||
dumps_from_archive = {
|
||||
dumps_from_archive = (
|
||||
module.Dump('mariadb_databases', 'foo'),
|
||||
module.Dump('postgresql_databases', 'foo'),
|
||||
module.Dump('sqlite_databases', 'bar'),
|
||||
}
|
||||
)
|
||||
flexmock(module).should_receive('dumps_match').and_return(False)
|
||||
flexmock(module).should_receive('dumps_match').with_args(
|
||||
module.Dump(
|
||||
@@ -1055,17 +1066,15 @@ def test_get_dumps_to_restore_with_requested_shortened_hook_name_filters_dumps_f
|
||||
original_container=None,
|
||||
),
|
||||
dumps_from_archive=dumps_from_archive,
|
||||
) == {
|
||||
module.Dump('postgresql_databases', 'foo'),
|
||||
}
|
||||
) == (module.Dump('postgresql_databases', 'foo'),)
|
||||
|
||||
|
||||
def test_get_dumps_to_restore_with_requested_hostname_filters_dumps_found_in_archive():
|
||||
dumps_from_archive = {
|
||||
dumps_from_archive = (
|
||||
module.Dump('postgresql_databases', 'foo'),
|
||||
module.Dump('postgresql_databases', 'foo', 'host'),
|
||||
module.Dump('postgresql_databases', 'bar'),
|
||||
}
|
||||
)
|
||||
flexmock(module).should_receive('dumps_match').and_return(False)
|
||||
flexmock(module).should_receive('dumps_match').with_args(
|
||||
module.Dump(
|
||||
@@ -1088,17 +1097,15 @@ def test_get_dumps_to_restore_with_requested_hostname_filters_dumps_found_in_arc
|
||||
original_container=None,
|
||||
),
|
||||
dumps_from_archive=dumps_from_archive,
|
||||
) == {
|
||||
module.Dump('postgresql_databases', 'foo', 'host'),
|
||||
}
|
||||
) == (module.Dump('postgresql_databases', 'foo', 'host'),)
|
||||
|
||||
|
||||
def test_get_dumps_to_restore_with_requested_port_filters_dumps_found_in_archive():
|
||||
dumps_from_archive = {
|
||||
dumps_from_archive = (
|
||||
module.Dump('postgresql_databases', 'foo', 'host'),
|
||||
module.Dump('postgresql_databases', 'foo', 'host', 1234),
|
||||
module.Dump('postgresql_databases', 'bar'),
|
||||
}
|
||||
)
|
||||
flexmock(module).should_receive('dumps_match').and_return(False)
|
||||
flexmock(module).should_receive('dumps_match').with_args(
|
||||
module.Dump(
|
||||
@@ -1122,9 +1129,7 @@ def test_get_dumps_to_restore_with_requested_port_filters_dumps_found_in_archive
|
||||
original_container=None,
|
||||
),
|
||||
dumps_from_archive=dumps_from_archive,
|
||||
) == {
|
||||
module.Dump('postgresql_databases', 'foo', 'host', 1234),
|
||||
}
|
||||
) == (module.Dump('postgresql_databases', 'foo', 'host', 1234),)
|
||||
|
||||
|
||||
def test_ensure_requested_dumps_restored_with_all_dumps_restored_does_not_raise():
|
||||
@@ -1143,8 +1148,8 @@ def test_ensure_requested_dumps_restored_with_all_dumps_restored_does_not_raise(
|
||||
def test_ensure_requested_dumps_restored_with_no_dumps_raises():
|
||||
with pytest.raises(ValueError):
|
||||
module.ensure_requested_dumps_restored(
|
||||
dumps_to_restore={},
|
||||
dumps_actually_restored={},
|
||||
dumps_to_restore=set(),
|
||||
dumps_actually_restored=set(),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ def test_validate_planned_backup_paths_skips_borgmatic_runtime_directory():
|
||||
) == ('/foo', '/baz')
|
||||
|
||||
|
||||
def test_validate_planned_backup_paths_with_borgmatic_runtime_directory_missing_from_paths_output_errors():
|
||||
def test_validate_planned_backup_paths_with_borgmatic_runtime_directory_missing_from_paths_output_warns():
|
||||
flexmock(module.flags).should_receive('omit_flag').replace_with(
|
||||
lambda arguments, flag: arguments,
|
||||
)
|
||||
@@ -136,26 +136,26 @@ def test_validate_planned_backup_paths_with_borgmatic_runtime_directory_missing_
|
||||
flexmock(module).should_receive('any_parent_directories').replace_with(
|
||||
lambda path, candidates: any(path.startswith(parent) for parent in candidates)
|
||||
)
|
||||
flexmock(module.logger).should_receive('warning').once()
|
||||
|
||||
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'),
|
||||
assert 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
|
||||
),
|
||||
local_path=None,
|
||||
working_directory=None,
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
module.borgmatic.borg.pattern.Pattern('/baz'),
|
||||
),
|
||||
local_path=None,
|
||||
working_directory=None,
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
) == ('/foo', '/bar', '/baz')
|
||||
|
||||
|
||||
def test_validate_planned_backup_paths_with_borgmatic_runtime_directory_partially_excluded_from_paths_output_errors():
|
||||
def test_validate_planned_backup_paths_with_borgmatic_runtime_directory_partially_excluded_from_paths_output_does_not_warn():
|
||||
flexmock(module.flags).should_receive('omit_flag').replace_with(
|
||||
lambda arguments, flag: arguments,
|
||||
)
|
||||
@@ -172,26 +172,63 @@ def test_validate_planned_backup_paths_with_borgmatic_runtime_directory_partiall
|
||||
flexmock(module).should_receive('any_parent_directories').replace_with(
|
||||
lambda path, candidates: any(path.startswith(parent) for parent in candidates)
|
||||
)
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
|
||||
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
|
||||
),
|
||||
assert 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
|
||||
),
|
||||
local_path=None,
|
||||
working_directory=None,
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
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',
|
||||
) == ('/foo', '/baz')
|
||||
|
||||
|
||||
def test_validate_planned_backup_paths_with_borgmatic_runtime_directory_with_corresponding_include_and_missing_from_paths_output_does_not_warn():
|
||||
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)
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
|
||||
'+ /foo\n- /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)
|
||||
)
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
|
||||
assert 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(
|
||||
'/run/borgmatic/bar', module.borgmatic.borg.pattern.Pattern_type.INCLUDE
|
||||
),
|
||||
module.borgmatic.borg.pattern.Pattern('/baz'),
|
||||
),
|
||||
local_path=None,
|
||||
working_directory=None,
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
) == ('/foo', '/bar', '/baz')
|
||||
|
||||
|
||||
def test_validate_planned_backup_paths_with_borgmatic_runtime_directory_missing_from_patterns_does_not_raise():
|
||||
|
||||
@@ -685,23 +685,7 @@ def test_extract_archive_with_log_json_and_progress_calls_borg_with_both_flags()
|
||||
)
|
||||
|
||||
|
||||
def test_extract_archive_with_progress_and_extract_to_stdout_raises():
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.extract_archive(
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
paths=None,
|
||||
config={'progress': True},
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
extract_to_stdout=True,
|
||||
)
|
||||
|
||||
|
||||
def test_extract_archive_calls_borg_with_stdout_parameter_and_returns_process():
|
||||
def test_extract_archive_calls_borg_with_extract_to_stdout_returns_process():
|
||||
flexmock(module.os.path).should_receive('abspath').and_return('repo')
|
||||
process = flexmock()
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
@@ -739,6 +723,44 @@ def test_extract_archive_calls_borg_with_stdout_parameter_and_returns_process():
|
||||
)
|
||||
|
||||
|
||||
def test_extract_archive_with_progress_and_extract_to_stdout_ignores_progress():
|
||||
flexmock(module.os.path).should_receive('abspath').and_return('repo')
|
||||
process = flexmock()
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'extract', '--stdout', 'repo::archive'),
|
||||
output_file=module.subprocess.PIPE,
|
||||
run_to_completion=False,
|
||||
environment=None,
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).and_return(process).once()
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
||||
('repo::archive',),
|
||||
)
|
||||
flexmock(module.borgmatic.config.validate).should_receive(
|
||||
'normalize_repository_path',
|
||||
).and_return('repo')
|
||||
|
||||
assert (
|
||||
module.extract_archive(
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
paths=None,
|
||||
config={'progress': True},
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
extract_to_stdout=True,
|
||||
)
|
||||
== process
|
||||
)
|
||||
|
||||
|
||||
def test_extract_archive_skips_abspath_for_remote_repository():
|
||||
flexmock(module.os.path).should_receive('abspath').never()
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
|
||||
@@ -157,6 +157,6 @@ def test_remove_data_source_dumps_without_dump_path_present_skips_removal():
|
||||
|
||||
def test_convert_glob_patterns_to_borg_pattern_makes_multipart_regular_expression():
|
||||
assert (
|
||||
module.convert_glob_patterns_to_borg_pattern(('/etc/foo/bar', '/bar/*/baz'))
|
||||
== 're:(?s:etc/foo/bar)|(?s:bar/.*/baz)'
|
||||
module.convert_glob_patterns_to_borg_pattern(('/etc/foo/bar', '/bar/baz/quux'))
|
||||
== 're:(?s:etc/foo/bar)$|(?s:etc/foo/bar/.*)$|(?s:bar/baz/quux)$|(?s:bar/baz/quux/.*)$'
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user