diff --git a/NEWS b/NEWS index 364e82e6..74aa862e 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,9 @@ 1.9.2.dev0 * #932: Fix missing build backend setting in pyproject.toml to allow Fedora builds. + * #934: Update the logic that probes for the borgmatic streaming database dump, bootstrap + metadata, and check state directories to support more platforms and use cases. + * #934: Add the "RuntimeDirectory" and "StateDirectory" options to the sample systemd service + file to support the new runtime and state directory logic. 1.9.1 * #928: Fix the user runtime directory location on macOS (and possibly Cygwin). diff --git a/borgmatic/actions/check.py b/borgmatic/actions/check.py index d5f7fbe2..928ac481 100644 --- a/borgmatic/actions/check.py +++ b/borgmatic/actions/check.py @@ -403,16 +403,22 @@ BORG_DIRECTORY_FILE_TYPE = 'd' def collect_spot_check_archive_paths( - repository, archive, config, local_borg_version, global_arguments, local_path, remote_path + repository, + archive, + config, + local_borg_version, + global_arguments, + local_path, + remote_path, + borgmatic_runtime_directory, ): ''' Given a repository configuration dict, the name of the latest archive, a configuration dict, the - local Borg version, global arguments as an argparse.Namespace instance, the local Borg path, and - the remote Borg path, collect the paths from the given archive (but only include files and - symlinks and exclude borgmatic runtime directories). + local Borg version, global arguments as an argparse.Namespace instance, the local Borg path, the + remote Borg path, and the borgmatic runtime directory, collect the paths from the given archive + (but only include files and symlinks and exclude borgmatic runtime directories). ''' borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config) - borgmatic_runtime_directory = borgmatic.config.paths.get_borgmatic_runtime_directory(config) return tuple( path @@ -445,7 +451,7 @@ def compare_spot_check_hashes( global_arguments, local_path, remote_path, - log_label, + log_prefix, source_paths, ): ''' @@ -469,7 +475,7 @@ def compare_spot_check_hashes( if os.path.exists(os.path.join(working_directory or '', source_path)) } logger.debug( - f'{log_label}: Sampling {sample_count} source paths (~{spot_check_config["data_sample_percentage"]}%) for spot check' + f'{log_prefix}: Sampling {sample_count} source paths (~{spot_check_config["data_sample_percentage"]}%) for spot check' ) source_sample_paths_iterator = iter(source_sample_paths) @@ -546,18 +552,19 @@ def spot_check( global_arguments, local_path, remote_path, + borgmatic_runtime_directory, ): ''' Given a repository dict, a loaded configuration dict, the local Borg version, global arguments - as an argparse.Namespace instance, the local Borg path, and the remote Borg path, perform a spot - check for the latest archive in the given repository. + as an argparse.Namespace instance, the local Borg path, the remote Borg path, and the borgmatic + runtime directory, perform a spot check for the latest archive in the given repository. A spot check compares file counts and also the hashes for a random sampling of source files on disk to those stored in the latest archive. If any differences are beyond configured tolerances, then the check fails. ''' - log_label = f'{repository.get("label", repository["path"])}' - logger.debug(f'{log_label}: Running spot check') + log_prefix = f'{repository.get("label", repository["path"])}' + logger.debug(f'{log_prefix}: Running spot check') try: spot_check_config = next( @@ -579,7 +586,7 @@ def spot_check( local_path, remote_path, ) - logger.debug(f'{log_label}: {len(source_paths)} total source paths for spot check') + logger.debug(f'{log_prefix}: {len(source_paths)} total source paths for spot check') archive = borgmatic.borg.repo_list.resolve_archive_name( repository['path'], @@ -590,7 +597,7 @@ def spot_check( local_path, remote_path, ) - logger.debug(f'{log_label}: Using archive {archive} for spot check') + logger.debug(f'{log_prefix}: Using archive {archive} for spot check') archive_paths = collect_spot_check_archive_paths( repository, @@ -600,8 +607,9 @@ def spot_check( global_arguments, local_path, remote_path, + borgmatic_runtime_directory, ) - logger.debug(f'{log_label}: {len(archive_paths)} total archive paths for spot check') + logger.debug(f'{log_prefix}: {len(archive_paths)} total archive paths for spot check') # Calculate the percentage delta between the source paths count and the archive paths count, and # compare that delta to the configured count tolerance percentage. @@ -609,10 +617,10 @@ def spot_check( if count_delta_percentage > spot_check_config['count_tolerance_percentage']: logger.debug( - f'{log_label}: Paths in source paths but not latest archive: {", ".join(set(source_paths) - set(archive_paths)) or "none"}' + f'{log_prefix}: Paths in source paths but not latest archive: {", ".join(set(source_paths) - set(archive_paths)) or "none"}' ) logger.debug( - f'{log_label}: Paths in latest archive but not source paths: {", ".join(set(archive_paths) - set(source_paths)) or "none"}' + f'{log_prefix}: Paths in latest archive but not source paths: {", ".join(set(archive_paths) - set(source_paths)) or "none"}' ) raise ValueError( f'Spot check failed: {count_delta_percentage:.2f}% file count delta between source paths and latest archive (tolerance is {spot_check_config["count_tolerance_percentage"]}%)' @@ -626,25 +634,25 @@ def spot_check( global_arguments, local_path, remote_path, - log_label, + log_prefix, source_paths, ) # Error if the percentage of failing hashes exceeds the configured tolerance percentage. - logger.debug(f'{log_label}: {len(failing_paths)} non-matching spot check hashes') + logger.debug(f'{log_prefix}: {len(failing_paths)} non-matching spot check hashes') data_tolerance_percentage = spot_check_config['data_tolerance_percentage'] failing_percentage = (len(failing_paths) / len(source_paths)) * 100 if failing_percentage > data_tolerance_percentage: logger.debug( - f'{log_label}: Source paths with data not matching the latest archive: {", ".join(failing_paths)}' + f'{log_prefix}: Source paths with data not matching the latest archive: {", ".join(failing_paths)}' ) raise ValueError( f'Spot check failed: {failing_percentage:.2f}% of source paths with data not matching the latest archive (tolerance is {data_tolerance_percentage}%)' ) logger.info( - f'{log_label}: Spot check passed with a {count_delta_percentage:.2f}% file count delta and a {failing_percentage:.2f}% file data delta' + f'{log_prefix}: Spot check passed with a {count_delta_percentage:.2f}% file count delta and a {failing_percentage:.2f}% file data delta' ) @@ -678,7 +686,9 @@ def run_check( **hook_context, ) - logger.info(f'{repository.get("label", repository["path"])}: Running consistency checks') + log_prefix = repository.get('label', repository['path']) + logger.info(f'{log_prefix}: Running consistency checks') + repository_id = borgmatic.borg.check.get_repository_id( repository['path'], config, @@ -730,14 +740,18 @@ def run_check( write_check_time(make_check_time_path(config, repository_id, 'extract')) if 'spot' in checks: - spot_check( - repository, - config, - local_borg_version, - global_arguments, - local_path, - remote_path, - ) + with borgmatic.config.paths.Runtime_directory( + config, log_prefix + ) as borgmatic_runtime_directory: + spot_check( + repository, + config, + local_borg_version, + global_arguments, + local_path, + remote_path, + borgmatic_runtime_directory, + ) write_check_time(make_check_time_path(config, repository_id, 'spot')) borgmatic.hooks.command.execute_hook( diff --git a/borgmatic/actions/config/bootstrap.py b/borgmatic/actions/config/bootstrap.py index bd1c6bde..17cf60f5 100644 --- a/borgmatic/actions/config/bootstrap.py +++ b/borgmatic/actions/config/bootstrap.py @@ -38,37 +38,44 @@ def get_config_paths(archive_name, bootstrap_arguments, global_arguments, local_ borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory( {'borgmatic_source_directory': bootstrap_arguments.borgmatic_source_directory} ) - borgmatic_runtime_directory = borgmatic.config.paths.get_borgmatic_runtime_directory( - {'user_runtime_directory': bootstrap_arguments.user_runtime_directory} - ) config = make_bootstrap_config(bootstrap_arguments) # Probe for the manifest file in multiple locations, as the default location has moved to the # borgmatic runtime directory (which get stored as just "/borgmatic" with Borg 1.4+). But we # still want to support reading the manifest from previously created archives as well. - for base_directory in ('borgmatic', borgmatic_runtime_directory, borgmatic_source_directory): - borgmatic_manifest_path = os.path.join(base_directory, 'bootstrap', 'manifest.json') + with borgmatic.config.paths.Runtime_directory( + {'user_runtime_directory': bootstrap_arguments.user_runtime_directory}, + bootstrap_arguments.repository, + ) as borgmatic_runtime_directory: + for base_directory in ( + 'borgmatic', + borgmatic.config.paths.make_runtime_directory_glob(borgmatic_runtime_directory), + borgmatic_source_directory, + ): + borgmatic_manifest_path = 'sh:' + os.path.join( + base_directory, 'bootstrap', 'manifest.json' + ) - extract_process = borgmatic.borg.extract.extract_archive( - global_arguments.dry_run, - bootstrap_arguments.repository, - archive_name, - [borgmatic_manifest_path], - config, - local_borg_version, - global_arguments, - local_path=bootstrap_arguments.local_path, - remote_path=bootstrap_arguments.remote_path, - extract_to_stdout=True, - ) - manifest_json = extract_process.stdout.read() + extract_process = borgmatic.borg.extract.extract_archive( + global_arguments.dry_run, + bootstrap_arguments.repository, + archive_name, + [borgmatic_manifest_path], + config, + local_borg_version, + global_arguments, + local_path=bootstrap_arguments.local_path, + remote_path=bootstrap_arguments.remote_path, + extract_to_stdout=True, + ) + manifest_json = extract_process.stdout.read() - if manifest_json: - break - else: - raise ValueError( - 'Cannot read configuration paths from archive due to missing bootstrap manifest' - ) + if manifest_json: + break + else: + raise ValueError( + 'Cannot read configuration paths from archive due to missing bootstrap manifest' + ) try: manifest_data = json.loads(manifest_json) diff --git a/borgmatic/actions/create.py b/borgmatic/actions/create.py index aa8d6bc6..03a5ca52 100644 --- a/borgmatic/actions/create.py +++ b/borgmatic/actions/create.py @@ -14,15 +14,15 @@ import borgmatic.hooks.dump logger = logging.getLogger(__name__) -def create_borgmatic_manifest(config, config_paths, dry_run): +def create_borgmatic_manifest(config, config_paths, borgmatic_runtime_directory, dry_run): ''' - Create a borgmatic manifest file to store the paths to the configuration files used to create - the archive. + Given a configuration dict, a sequence of config file paths, the borgmatic runtime directory, + and whether this is a dry run, create a borgmatic manifest file to store the paths to the + configuration files used to create the archive. ''' if dry_run: return - borgmatic_runtime_directory = borgmatic.config.paths.get_borgmatic_runtime_directory(config) borgmatic_manifest_path = os.path.join( borgmatic_runtime_directory, 'bootstrap', 'manifest.json' ) @@ -71,54 +71,68 @@ def run_create( global_arguments.dry_run, **hook_context, ) - logger.info(f'{repository.get("label", repository["path"])}: Creating archive{dry_run_label}') - borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured( - 'remove_data_source_dumps', - config, - repository['path'], - borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES, - global_arguments.dry_run, - ) - active_dumps = borgmatic.hooks.dispatch.call_hooks( - 'dump_data_sources', - config, - repository['path'], - borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES, - global_arguments.dry_run, - ) - if config.get('store_config_files', True): - create_borgmatic_manifest( + + log_prefix = repository.get('label', repository['path']) + logger.info(f'{log_prefix}: Creating archive{dry_run_label}') + + with borgmatic.config.paths.Runtime_directory( + config, log_prefix + ) as borgmatic_runtime_directory: + borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured( + 'remove_data_source_dumps', config, - config_paths, + repository['path'], + borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES, + borgmatic_runtime_directory, global_arguments.dry_run, ) - stream_processes = [process for processes in active_dumps.values() for process in processes] + active_dumps = borgmatic.hooks.dispatch.call_hooks( + 'dump_data_sources', + config, + repository['path'], + borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES, + borgmatic_runtime_directory, + global_arguments.dry_run, + ) + stream_processes = [process for processes in active_dumps.values() for process in processes] - json_output = borgmatic.borg.create.create_archive( - global_arguments.dry_run, - repository['path'], - config, - config_paths, - local_borg_version, - global_arguments, - local_path=local_path, - remote_path=remote_path, - progress=create_arguments.progress, - stats=create_arguments.stats, - json=create_arguments.json, - list_files=create_arguments.list_files, - stream_processes=stream_processes, - ) - if json_output: - yield borgmatic.actions.json.parse_json(json_output, repository.get('label')) + if config.get('store_config_files', True): + create_borgmatic_manifest( + config, + config_paths, + borgmatic_runtime_directory, + global_arguments.dry_run, + ) + + json_output = borgmatic.borg.create.create_archive( + global_arguments.dry_run, + repository['path'], + config, + config_paths, + local_borg_version, + global_arguments, + borgmatic_runtime_directory, + local_path=local_path, + remote_path=remote_path, + progress=create_arguments.progress, + stats=create_arguments.stats, + json=create_arguments.json, + list_files=create_arguments.list_files, + stream_processes=stream_processes, + ) + + if json_output: + yield borgmatic.actions.json.parse_json(json_output, repository.get('label')) + + borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured( + 'remove_data_source_dumps', + config, + config_filename, + borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES, + borgmatic_runtime_directory, + global_arguments.dry_run, + ) - borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured( - 'remove_data_source_dumps', - config, - config_filename, - borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES, - global_arguments.dry_run, - ) borgmatic.hooks.command.execute_hook( config.get('after_backup'), config.get('umask'), diff --git a/borgmatic/actions/restore.py b/borgmatic/actions/restore.py index 3441002c..0010b250 100644 --- a/borgmatic/actions/restore.py +++ b/borgmatic/actions/restore.py @@ -108,6 +108,7 @@ def restore_single_data_source( hook_name, data_source, connection_params, + borgmatic_runtime_directory, ): ''' Given (among other things) an archive name, a data source hook name, the hostname, port, @@ -123,9 +124,9 @@ def restore_single_data_source( config, repository['path'], borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES, + borgmatic_runtime_directory, data_source['name'], )[hook_name] - borgmatic_runtime_directory = borgmatic.config.paths.get_borgmatic_runtime_directory(config) destination_path = ( tempfile.mkdtemp(dir=borgmatic_runtime_directory) @@ -135,7 +136,7 @@ def restore_single_data_source( try: # Kick off a single data source extract. If using a directory format, extract to a temporary - # directory. Otheriwes extract the single dump file to stdout. + # directory. Otherwise extract the single dump file to stdout. extract_process = borgmatic.borg.extract.extract_archive( dry_run=global_arguments.dry_run, repository=repository['path'], @@ -170,6 +171,7 @@ def restore_single_data_source( dry_run=global_arguments.dry_run, extract_process=extract_process, connection_params=connection_params, + borgmatic_runtime_directory=borgmatic_runtime_directory, ) @@ -181,17 +183,17 @@ def collect_archive_data_source_names( global_arguments, local_path, remote_path, + borgmatic_runtime_directory, ): ''' Given a local or remote repository path, a resolved archive name, a configuration dict, the - local Borg version, global_arguments an argparse.Namespace, and local and remote Borg paths, - query the archive for the names of data sources it contains as dumps and return them as a dict - from hook name to a sequence of data source names. + local Borg version, global_arguments an argparse.Namespace, local and remote Borg paths, and the + borgmatic runtime directory, query the archive for the names of data sources it contains as + dumps and return them as a dict from hook name to a sequence of data source names. ''' borgmatic_source_directory = str( pathlib.Path(borgmatic.config.paths.get_borgmatic_source_directory(config)) ) - borgmatic_runtime_directory = borgmatic.config.paths.get_borgmatic_runtime_directory(config) # Probe for the data source dumps in multiple locations, as the default location has moved to # the borgmatic runtime directory (which get stored as just "/borgmatic" with Borg 1.4+). But we @@ -207,7 +209,7 @@ def collect_archive_data_source_names( + borgmatic.hooks.dump.make_data_source_dump_path(base_directory, '*_databases/*/*') for base_directory in ( 'borgmatic', - borgmatic_runtime_directory.lstrip('/'), + borgmatic.config.paths.make_runtime_directory_glob(borgmatic_runtime_directory), borgmatic_source_directory.lstrip('/'), ) ], @@ -342,109 +344,116 @@ def run_restore( ): return - logger.info( - f'{repository.get("label", repository["path"])}: Restoring data sources from archive {restore_arguments.archive}' - ) + log_prefix = repository.get('label', repository['path']) + logger.info(f'{log_prefix}: Restoring data sources from archive {restore_arguments.archive}') - borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured( - 'remove_data_source_dumps', - config, - repository['path'], - borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES, - global_arguments.dry_run, - ) + with borgmatic.config.paths.Runtime_directory( + config, log_prefix + ) as borgmatic_runtime_directory: + borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured( + 'remove_data_source_dumps', + config, + repository['path'], + borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES, + borgmatic_runtime_directory, + global_arguments.dry_run, + ) - archive_name = borgmatic.borg.repo_list.resolve_archive_name( - repository['path'], - restore_arguments.archive, - config, - local_borg_version, - global_arguments, - local_path, - remote_path, - ) - archive_data_source_names = collect_archive_data_source_names( - repository['path'], - archive_name, - config, - local_borg_version, - global_arguments, - local_path, - remote_path, - ) - restore_names = find_data_sources_to_restore( - restore_arguments.data_sources, archive_data_source_names - ) - found_names = set() - remaining_restore_names = {} - connection_params = { - 'hostname': restore_arguments.hostname, - 'port': restore_arguments.port, - 'username': restore_arguments.username, - 'password': restore_arguments.password, - 'restore_path': restore_arguments.restore_path, - } + archive_name = borgmatic.borg.repo_list.resolve_archive_name( + repository['path'], + restore_arguments.archive, + config, + local_borg_version, + global_arguments, + local_path, + remote_path, + ) + archive_data_source_names = collect_archive_data_source_names( + repository['path'], + archive_name, + config, + local_borg_version, + global_arguments, + local_path, + remote_path, + borgmatic_runtime_directory, + ) + restore_names = find_data_sources_to_restore( + restore_arguments.data_sources, archive_data_source_names + ) + found_names = set() + remaining_restore_names = {} + connection_params = { + 'hostname': restore_arguments.hostname, + 'port': restore_arguments.port, + 'username': restore_arguments.username, + 'password': restore_arguments.password, + 'restore_path': restore_arguments.restore_path, + } - for hook_name, data_source_names in restore_names.items(): - for data_source_name in data_source_names: - found_hook_name, found_data_source = get_configured_data_source( - config, archive_data_source_names, hook_name, data_source_name - ) - - if not found_data_source: - remaining_restore_names.setdefault(found_hook_name or hook_name, []).append( - data_source_name + for hook_name, data_source_names in restore_names.items(): + for data_source_name in data_source_names: + found_hook_name, found_data_source = get_configured_data_source( + config, archive_data_source_names, hook_name, data_source_name ) - continue - found_names.add(data_source_name) - restore_single_data_source( - repository, - config, - local_borg_version, - global_arguments, - local_path, - remote_path, - archive_name, - found_hook_name or hook_name, - dict(found_data_source, **{'schemas': restore_arguments.schemas}), - connection_params, - ) + if not found_data_source: + remaining_restore_names.setdefault(found_hook_name or hook_name, []).append( + data_source_name + ) + continue - # For any data sources that weren't found via exact matches in the configuration, try to - # fallback to "all" entries. - for hook_name, data_source_names in remaining_restore_names.items(): - for data_source_name in data_source_names: - found_hook_name, found_data_source = get_configured_data_source( - config, archive_data_source_names, hook_name, data_source_name, 'all' - ) + found_names.add(data_source_name) + restore_single_data_source( + repository, + config, + local_borg_version, + global_arguments, + local_path, + remote_path, + archive_name, + found_hook_name or hook_name, + dict(found_data_source, **{'schemas': restore_arguments.schemas}), + connection_params, + borgmatic_runtime_directory, + ) - if not found_data_source: - continue + # For any data sources that weren't found via exact matches in the configuration, try to + # fallback to "all" entries. + for hook_name, data_source_names in remaining_restore_names.items(): + for data_source_name in data_source_names: + found_hook_name, found_data_source = get_configured_data_source( + config, archive_data_source_names, hook_name, data_source_name, 'all' + ) - found_names.add(data_source_name) - data_source = copy.copy(found_data_source) - data_source['name'] = data_source_name + if not found_data_source: + continue - restore_single_data_source( - repository, - config, - local_borg_version, - global_arguments, - local_path, - remote_path, - archive_name, - found_hook_name or hook_name, - dict(data_source, **{'schemas': restore_arguments.schemas}), - connection_params, - ) + found_names.add(data_source_name) + data_source = copy.copy(found_data_source) + data_source['name'] = data_source_name - borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured( - 'remove_data_source_dumps', - config, - repository['path'], - borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES, - global_arguments.dry_run, - ) + restore_single_data_source( + repository, + config, + local_borg_version, + global_arguments, + local_path, + remote_path, + archive_name, + found_hook_name or hook_name, + dict(data_source, **{'schemas': restore_arguments.schemas}), + connection_params, + borgmatic_runtime_directory, + ) + + borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured( + 'remove_data_source_dumps', + config, + repository['path'], + borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES, + borgmatic_runtime_directory, + global_arguments.dry_run, + ) ensure_data_sources_found(restore_names, remaining_restore_names, found_names) diff --git a/borgmatic/borg/create.py b/borgmatic/borg/create.py index c34f7dfd..9eacf4d2 100644 --- a/borgmatic/borg/create.py +++ b/borgmatic/borg/create.py @@ -504,6 +504,7 @@ def create_archive( config_paths, local_borg_version, global_arguments, + borgmatic_runtime_directory, local_path='borg', remote_path=None, progress=False, @@ -524,9 +525,7 @@ def create_archive( working_directory = borgmatic.config.paths.get_working_directory(config) borgmatic_runtime_directories = expand_directories( - collect_borgmatic_runtime_directories( - borgmatic.config.paths.get_borgmatic_runtime_directory(config) - ), + collect_borgmatic_runtime_directories(borgmatic_runtime_directory), working_directory=working_directory, ) diff --git a/borgmatic/config/paths.py b/borgmatic/config/paths.py index ad318d6d..0b13e65d 100644 --- a/borgmatic/config/paths.py +++ b/borgmatic/config/paths.py @@ -1,4 +1,8 @@ +import logging import os +import tempfile + +logger = logging.getLogger(__name__) def expand_user_in_path(path): @@ -26,26 +30,91 @@ def get_borgmatic_source_directory(config): return expand_user_in_path(config.get('borgmatic_source_directory') or '~/.borgmatic') -def get_borgmatic_runtime_directory(config): - ''' - Given a configuration dict, get the borgmatic runtime directory used for storing temporary - runtime data like streaming database dumps and bootstrap metadata. Defaults to - $XDG_RUNTIME_DIR/./borgmatic or $TMPDIR/./borgmatic or $TEMP/./borgmatic or - /run/user/$UID/./borgmatic. +TEMPORARY_DIRECTORY_PREFIX = 'borgmatic-' - The "/./" is taking advantage of a Borg feature such that the part of the path before the "/./" - does not get stored in the file path within an archive. That way, the path of the runtime - directory can change without leaving database dumps within an archive inaccessible. + +class Runtime_directory: ''' - return expand_user_in_path( - os.path.join( + A Python context manager for creating and cleaning up the borgmatic runtime directory used for + storing temporary runtime data like streaming database dumps and bootstrap metadata. + + Example use as a context manager: + + with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory: + do_something_with(borgmatic_runtime_directory) + + For the scope of that "with" statement, the runtime directory is available. Afterwards, it + automatically gets cleaned up as necessary. + ''' + + def __init__(self, config, log_prefix): + ''' + Given a configuration dict and a log prefix, determine the borgmatic runtime directory, + creating a secure, temporary directory within it if necessary. Defaults to + $XDG_RUNTIME_DIR/./borgmatic or $RUNTIME_DIRECTORY/./borgmatic or + $TMPDIR/borgmatic-[random]/./borgmatic or $TEMP/borgmatic-[random]/./borgmatic or + /tmp/borgmatic-[random]/./borgmatic where "[random]" is a randomly generated string intended + to avoid path collisions. + + If XDG_RUNTIME_DIR or RUNTIME_DIRECTORY is set and already ends in "/borgmatic", then don't + tack on a second "/borgmatic" path component. + + The "/./" is taking advantage of a Borg feature such that the part of the path before the "/./" + does not get stored in the file path within an archive. That way, the path of the runtime + directory can change without leaving database dumps within an archive inaccessible. + ''' + runtime_directory = ( config.get('user_runtime_directory') - or os.environ.get('XDG_RUNTIME_DIR') - or os.environ.get('TMPDIR') - or os.environ.get('TEMP') - or f'/run/user/{os.getuid()}', - '.', - 'borgmatic', + or os.environ.get('XDG_RUNTIME_DIR') # Set by PAM on Linux. + or os.environ.get('RUNTIME_DIRECTORY') # Set by systemd if configured. + ) + + if runtime_directory: + self.temporary_directory = None + else: + base_directory = os.environ.get('TMPDIR') or os.environ.get('TEMP') or '/tmp' + os.makedirs(base_directory, mode=0o700, exist_ok=True) + self.temporary_directory = tempfile.TemporaryDirectory( + prefix=TEMPORARY_DIRECTORY_PREFIX, + dir=base_directory, + ) + runtime_directory = self.temporary_directory.name + + (base_path, final_directory) = os.path.split(runtime_directory.rstrip(os.path.sep)) + + self.runtime_path = expand_user_in_path( + os.path.join( + base_path if final_directory == 'borgmatic' else runtime_directory, '.', 'borgmatic' + ) + ) + os.makedirs(self.runtime_path, mode=0o700, exist_ok=True) + + logger.debug(f'{log_prefix}: Using runtime directory {os.path.normpath(self.runtime_path)}') + + def __enter__(self): + ''' + Return the borgmatic runtime path as a string. + ''' + return self.runtime_path + + def __exit__(self, exception, value, traceback): + ''' + Delete any temporary directory that was created as part of initialization. + ''' + if self.temporary_directory: + self.temporary_directory.cleanup() + + +def make_runtime_directory_glob(borgmatic_runtime_directory): + ''' + Given a borgmatic runtime directory path, make a glob that would match that path, specifically + replacing any randomly generated temporary subdirectory with "*" since such a directory's name + changes on every borgmatic run. + ''' + return os.path.join( + *( + '*' if subdirectory.startswith(TEMPORARY_DIRECTORY_PREFIX) else subdirectory + for subdirectory in os.path.normpath(borgmatic_runtime_directory).split(os.path.sep) ) ) @@ -59,10 +128,9 @@ def get_borgmatic_state_directory(config): return expand_user_in_path( os.path.join( config.get('user_state_directory') - or os.environ.get( - 'XDG_STATE_HOME', - '~/.local/state', - ), + or os.environ.get('XDG_STATE_HOME') + or os.environ.get('STATE_DIRECTORY') # Set by systemd if configured. + or '~/.local/state', 'borgmatic', ) ) diff --git a/borgmatic/hooks/mariadb.py b/borgmatic/hooks/mariadb.py index dcbd9d4b..fa8e9960 100644 --- a/borgmatic/hooks/mariadb.py +++ b/borgmatic/hooks/mariadb.py @@ -14,15 +14,11 @@ from borgmatic.hooks import dump logger = logging.getLogger(__name__) -def make_dump_path(config, base_directory=None): # pragma: no cover +def make_dump_path(base_directory): # pragma: no cover ''' - Given a configuration dict and an optional base directory, make the corresponding dump path. If - a base directory isn't provided, use the borgmatic runtime directory. + Given a base directory, make the corresponding dump path. ''' - return dump.make_data_source_dump_path( - base_directory or borgmatic.config.paths.get_borgmatic_runtime_directory(config), - 'mariadb_databases', - ) + return dump.make_data_source_dump_path(base_directory, 'mariadb_databases') SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys') @@ -126,12 +122,12 @@ def use_streaming(databases, config, log_prefix): return any(databases) -def dump_data_sources(databases, config, log_prefix, dry_run): +def dump_data_sources(databases, config, log_prefix, borgmatic_runtime_directory, dry_run): ''' Dump the given MariaDB databases to a named pipe. The databases are supplied as a sequence of dicts, one dict describing each database as per the configuration schema. Use the given - configuration dict to construct the destination path and the given log prefix in any log - entries. + borgmatic runtime directory to construct the destination path and the given log prefix in any + log entries. Return a sequence of subprocess.Popen instances for the dump processes ready to spew to a named pipe. But if this is a dry run, then don't actually dump anything and return an empty sequence. @@ -142,7 +138,7 @@ def dump_data_sources(databases, config, log_prefix, dry_run): logger.info(f'{log_prefix}: Dumping MariaDB databases{dry_run_label}') for database in databases: - dump_path = make_dump_path(config) + dump_path = make_dump_path(borgmatic_runtime_directory) extra_environment = {'MYSQL_PWD': database['password']} if 'password' in database else None dump_database_names = database_names_to_dump( database, extra_environment, log_prefix, dry_run @@ -185,43 +181,55 @@ def dump_data_sources(databases, config, log_prefix, dry_run): return [process for process in processes if process] -def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma: no cover +def remove_data_source_dumps( + databases, config, log_prefix, borgmatic_runtime_directory, dry_run +): # pragma: no cover ''' - Remove all database dump files for this hook regardless of the given databases. Use the given - configuration dict to construct the destination path and the log prefix in any log entries. If - this is a dry run, then don't actually remove anything. + Remove all database dump files for this hook regardless of the given databases. Use the + borgmatic_runtime_directory to construct the destination path and the log prefix in any log + entries. If this is a dry run, then don't actually remove anything. ''' - dump.remove_data_source_dumps(make_dump_path(config), 'MariaDB', log_prefix, dry_run) + dump.remove_data_source_dumps( + make_dump_path(borgmatic_runtime_directory), 'MariaDB', log_prefix, dry_run + ) -def make_data_source_dump_patterns(databases, config, log_prefix, name=None): # pragma: no cover +def make_data_source_dump_patterns( + databases, config, log_prefix, borgmatic_runtime_directory, name=None +): # pragma: no cover ''' - Given a sequence of configurations dicts, a configuration dict, a prefix to log with, and a - database name to match, return the corresponding glob patterns to match the database dump in an - archive. + Given a sequence of configurations dicts, a configuration dict, a prefix to log with, the + borgmatic runtime directory, and a database name to match, return the corresponding glob + patterns to match the database dump in an archive. ''' borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config) return ( + dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'), dump.make_data_source_dump_filename( - make_dump_path(config, 'borgmatic'), name, hostname='*' + make_dump_path(borgmatic_runtime_directory), name, hostname='*' ), - dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*'), dump.make_data_source_dump_filename( - make_dump_path(config, borgmatic_source_directory), name, hostname='*' + make_dump_path(borgmatic_source_directory), name, hostname='*' ), ) def restore_data_source_dump( - hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params + hook_config, + config, + log_prefix, + data_source, + dry_run, + extract_process, + connection_params, + borgmatic_runtime_directory, ): ''' Restore a database from the given extract stream. The database is supplied as a data source - configuration dict, but the given hook configuration is ignored. The given configuration dict is - used to construct the destination path, and the given log prefix is used for any log entries. If - this is a dry run, then don't actually restore anything. Trigger the given active extract - process (an instance of subprocess.Popen) to produce output to consume. + configuration dict, but the given hook configuration is ignored. The given log prefix is used + for any log entries. If this is a dry run, then don't actually restore anything. Trigger the + given active extract process (an instance of subprocess.Popen) to produce output to consume. ''' dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else '' hostname = connection_params['hostname'] or data_source.get( diff --git a/borgmatic/hooks/mongodb.py b/borgmatic/hooks/mongodb.py index 498b0638..49075823 100644 --- a/borgmatic/hooks/mongodb.py +++ b/borgmatic/hooks/mongodb.py @@ -8,15 +8,11 @@ from borgmatic.hooks import dump logger = logging.getLogger(__name__) -def make_dump_path(config, base_directory=None): # pragma: no cover +def make_dump_path(base_directory): # pragma: no cover ''' - Given a configuration dict and an optional base directory, make the corresponding dump path. If - a base directory isn't provided, use the borgmatic runtime directory. + Given a base directory, make the corresponding dump path. ''' - return dump.make_data_source_dump_path( - base_directory or borgmatic.config.paths.get_borgmatic_runtime_directory(config), - 'mongodb_databases', - ) + return dump.make_data_source_dump_path(base_directory, 'mongodb_databases') def use_streaming(databases, config, log_prefix): @@ -27,11 +23,12 @@ def use_streaming(databases, config, log_prefix): return any(database.get('format') != 'directory' for database in databases) -def dump_data_sources(databases, config, log_prefix, dry_run): +def dump_data_sources(databases, config, log_prefix, borgmatic_runtime_directory, dry_run): ''' Dump the given MongoDB databases to a named pipe. The databases are supplied as a sequence of - dicts, one dict describing each database as per the configuration schema. Use the configuration - dict to construct the destination path and the given log prefix in any log entries. + dicts, one dict describing each database as per the configuration schema. Use the borgmatic + runtime directory to construct the destination path (used for the directory format and the given + log prefix in any log entries. Return a sequence of subprocess.Popen instances for the dump processes ready to spew to a named pipe. But if this is a dry run, then don't actually dump anything and return an empty sequence. @@ -44,7 +41,7 @@ def dump_data_sources(databases, config, log_prefix, dry_run): for database in databases: name = database['name'] dump_filename = dump.make_data_source_dump_filename( - make_dump_path(config), name, database.get('hostname') + make_dump_path(borgmatic_runtime_directory), name, database.get('hostname') ) dump_format = database.get('format', 'archive') @@ -94,36 +91,49 @@ def build_dump_command(database, dump_filename, dump_format): ) -def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma: no cover +def remove_data_source_dumps( + databases, config, log_prefix, borgmatic_runtime_directory, dry_run +): # pragma: no cover ''' - Remove all database dump files for this hook regardless of the given databases. Use the log - prefix in any log entries. Use the given configuration dict to construct the destination path. - If this is a dry run, then don't actually remove anything. + Remove all database dump files for this hook regardless of the given databases. Use the + borgmatic_runtime_directory to construct the destination path and the log prefix in any log + entries. If this is a dry run, then don't actually remove anything. ''' - dump.remove_data_source_dumps(make_dump_path(config), 'MongoDB', log_prefix, dry_run) + dump.remove_data_source_dumps( + make_dump_path(borgmatic_runtime_directory), 'MongoDB', log_prefix, dry_run + ) -def make_data_source_dump_patterns(databases, config, log_prefix, name=None): # pragma: no cover +def make_data_source_dump_patterns( + databases, config, log_prefix, borgmatic_runtime_directory, name=None +): # pragma: no cover ''' - Given a sequence of database configurations dicts, a configuration dict, a prefix to log with, - and a database name to match, return the corresponding glob patterns to match the database dump - in an archive. + Given a sequence of configurations dicts, a configuration dict, a prefix to log with, the + borgmatic runtime directory, and a database name to match, return the corresponding glob + patterns to match the database dump in an archive. ''' borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config) return ( + dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'), dump.make_data_source_dump_filename( - make_dump_path(config, 'borgmatic'), name, hostname='*' + make_dump_path(borgmatic_runtime_directory), name, hostname='*' ), - dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*'), dump.make_data_source_dump_filename( - make_dump_path(config, borgmatic_source_directory), name, hostname='*' + make_dump_path(borgmatic_source_directory), name, hostname='*' ), ) def restore_data_source_dump( - hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params + hook_config, + config, + log_prefix, + data_source, + dry_run, + extract_process, + connection_params, + borgmatic_runtime_directory, ): ''' Restore a database from the given extract stream. The database is supplied as a data source @@ -137,7 +147,9 @@ def restore_data_source_dump( ''' dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else '' dump_filename = dump.make_data_source_dump_filename( - make_dump_path(config), data_source['name'], data_source.get('hostname') + make_dump_path(borgmatic_runtime_directory), + data_source['name'], + data_source.get('hostname'), ) restore_command = build_restore_command( extract_process, data_source, dump_filename, connection_params diff --git a/borgmatic/hooks/mysql.py b/borgmatic/hooks/mysql.py index fb72da0f..c8c89280 100644 --- a/borgmatic/hooks/mysql.py +++ b/borgmatic/hooks/mysql.py @@ -14,15 +14,11 @@ from borgmatic.hooks import dump logger = logging.getLogger(__name__) -def make_dump_path(config, base_directory=None): # pragma: no cover +def make_dump_path(base_directory): # pragma: no cover ''' - Given a configuration dict and an optional base directory, make the corresponding dump path. If - a base directory isn't provided, use the borgmatic runtime directory. + Given a base directory, make the corresponding dump path. ''' - return dump.make_data_source_dump_path( - base_directory or borgmatic.config.paths.get_borgmatic_runtime_directory(config), - 'mysql_databases', - ) + return dump.make_data_source_dump_path(base_directory, 'mysql_databases') SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys') @@ -125,11 +121,12 @@ def use_streaming(databases, config, log_prefix): return any(databases) -def dump_data_sources(databases, config, log_prefix, dry_run): +def dump_data_sources(databases, config, log_prefix, borgmatic_runtime_directory, dry_run): ''' Dump the given MySQL/MariaDB databases to a named pipe. The databases are supplied as a sequence of dicts, one dict describing each database as per the configuration schema. Use the given - configuration dict to construct the destination path and the given log prefix in any log entries. + borgmatic runtime directory to construct the destination path and the given log prefix in any + log entries. Return a sequence of subprocess.Popen instances for the dump processes ready to spew to a named pipe. But if this is a dry run, then don't actually dump anything and return an empty sequence. @@ -140,7 +137,7 @@ def dump_data_sources(databases, config, log_prefix, dry_run): logger.info(f'{log_prefix}: Dumping MySQL databases{dry_run_label}') for database in databases: - dump_path = make_dump_path(config) + dump_path = make_dump_path(borgmatic_runtime_directory) extra_environment = {'MYSQL_PWD': database['password']} if 'password' in database else None dump_database_names = database_names_to_dump( database, extra_environment, log_prefix, dry_run @@ -183,43 +180,55 @@ def dump_data_sources(databases, config, log_prefix, dry_run): return [process for process in processes if process] -def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma: no cover +def remove_data_source_dumps( + databases, config, log_prefix, borgmatic_runtime_directory, dry_run +): # pragma: no cover ''' - Remove all database dump files for this hook regardless of the given databases. Use the given - configuration dict to construct the destination path and the log prefix in any log entries. If - this is a dry run, then don't actually remove anything. + Remove all database dump files for this hook regardless of the given databases. Use the + borgmatic runtime directory to construct the destination path and the log prefix in any log + entries. If this is a dry run, then don't actually remove anything. ''' - dump.remove_data_source_dumps(make_dump_path(config), 'MySQL', log_prefix, dry_run) + dump.remove_data_source_dumps( + make_dump_path(borgmatic_runtime_directory), 'MySQL', log_prefix, dry_run + ) -def make_data_source_dump_patterns(databases, config, log_prefix, name=None): # pragma: no cover +def make_data_source_dump_patterns( + databases, config, log_prefix, borgmatic_runtime_directory, name=None +): # pragma: no cover ''' - Given a sequence of configurations dicts, a configuration dict, a prefix to log with, and a - database name to match, return the corresponding glob patterns to match the database dump in an - archive. + Given a sequence of configurations dicts, a configuration dict, a prefix to log with, the + borgmatic runtime directory, and a database name to match, return the corresponding glob + patterns to match the database dump in an archive. ''' borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config) return ( + dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'), dump.make_data_source_dump_filename( - make_dump_path(config, 'borgmatic'), name, hostname='*' + make_dump_path(borgmatic_runtime_directory), name, hostname='*' ), - dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*'), dump.make_data_source_dump_filename( - make_dump_path(config, borgmatic_source_directory), name, hostname='*' + make_dump_path(borgmatic_source_directory), name, hostname='*' ), ) def restore_data_source_dump( - hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params + hook_config, + config, + log_prefix, + data_source, + dry_run, + extract_process, + connection_params, + borgmatic_runtime_directory, ): ''' Restore a database from the given extract stream. The database is supplied as a data source - configuration dict, but the given hook configuration is ignored. The given configuration dict is - used to construct the destination path, and the given log prefix is used for any log entries. If - this is a dry run, then don't actually restore anything. Trigger the given active extract - process (an instance of subprocess.Popen) to produce output to consume. + configuration dict, but the given hook configuration is ignored. The given log prefix is used + for any log entries. If this is a dry run, then don't actually restore anything. Trigger the + given active extract process (an instance of subprocess.Popen) to produce output to consume. ''' dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else '' hostname = connection_params['hostname'] or data_source.get( diff --git a/borgmatic/hooks/postgresql.py b/borgmatic/hooks/postgresql.py index 1b510148..4067e09e 100644 --- a/borgmatic/hooks/postgresql.py +++ b/borgmatic/hooks/postgresql.py @@ -16,15 +16,11 @@ from borgmatic.hooks import dump logger = logging.getLogger(__name__) -def make_dump_path(config, base_directory=None): # pragma: no cover +def make_dump_path(base_directory): # pragma: no cover ''' - Given a configuration dict and an optional base directory, make the corresponding dump path. If - a base directory isn't provided, use the borgmatic runtime directory. + Given a base directory, make the corresponding dump path. ''' - return dump.make_data_source_dump_path( - base_directory or borgmatic.config.paths.get_borgmatic_runtime_directory(config), - 'postgresql_databases', - ) + return dump.make_data_source_dump_path(base_directory, 'postgresql_databases') def make_extra_environment(database, restore_connection_params=None): @@ -108,12 +104,12 @@ def use_streaming(databases, config, log_prefix): return any(database.get('format') != 'directory' for database in databases) -def dump_data_sources(databases, config, log_prefix, dry_run): +def dump_data_sources(databases, config, log_prefix, borgmatic_runtime_directory, dry_run): ''' Dump the given PostgreSQL databases to a named pipe. The databases are supplied as a sequence of dicts, one dict describing each database as per the configuration schema. Use the given - configuration dict to construct the destination path and the given log prefix in any log - entries. + borgmatic runtime directory to construct the destination path and the given log prefix in any + log entries. Return a sequence of subprocess.Popen instances for the dump processes ready to spew to a named pipe. But if this is a dry run, then don't actually dump anything and return an empty sequence. @@ -127,7 +123,7 @@ def dump_data_sources(databases, config, log_prefix, dry_run): for database in databases: extra_environment = make_extra_environment(database) - dump_path = make_dump_path(config) + dump_path = make_dump_path(borgmatic_runtime_directory) dump_database_names = database_names_to_dump( database, extra_environment, log_prefix, dry_run ) @@ -210,43 +206,57 @@ def dump_data_sources(databases, config, log_prefix, dry_run): return processes -def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma: no cover +def remove_data_source_dumps( + databases, config, log_prefix, borgmatic_runtime_directory, dry_run +): # pragma: no cover ''' - Remove all database dump files for this hook regardless of the given databases. Use the given - configuration dict to construct the destination path and the log prefix in any log entries. If - this is a dry run, then don't actually remove anything. + Remove all database dump files for this hook regardless of the given databases. Use the + borgmatic runtime directory to construct the destination path and the log prefix in any log + entries. If this is a dry run, then don't actually remove anything. ''' - dump.remove_data_source_dumps(make_dump_path(config), 'PostgreSQL', log_prefix, dry_run) + dump.remove_data_source_dumps( + make_dump_path(borgmatic_runtime_directory), 'PostgreSQL', log_prefix, dry_run + ) -def make_data_source_dump_patterns(databases, config, log_prefix, name=None): # pragma: no cover +def make_data_source_dump_patterns( + databases, config, log_prefix, borgmatic_runtime_directory, name=None +): # pragma: no cover ''' - Given a sequence of configurations dicts, a configuration dict, a prefix to log with, and a - database name to match, return the corresponding glob patterns to match the database dump in an - archive. + Given a sequence of configurations dicts, a configuration dict, a prefix to log with, the + borgmatic runtime directory, and a database name to match, return the corresponding glob + patterns to match the database dump in an archive. ''' borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config) return ( + dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'), dump.make_data_source_dump_filename( - make_dump_path(config, 'borgmatic'), name, hostname='*' + make_dump_path(borgmatic_runtime_directory), name, hostname='*' ), - dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*'), dump.make_data_source_dump_filename( - make_dump_path(config, borgmatic_source_directory), name, hostname='*' + make_dump_path(borgmatic_source_directory), name, hostname='*' ), ) def restore_data_source_dump( - hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params + hook_config, + config, + log_prefix, + data_source, + dry_run, + extract_process, + connection_params, + borgmatic_runtime_directory, ): ''' Restore a database from the given extract stream. The database is supplied as a data source - configuration dict, but the given hook configuration is ignored. The given configuration dict is - used to construct the destination path, and the given log prefix is used for any log entries. If - this is a dry run, then don't actually restore anything. Trigger the given active extract - process (an instance of subprocess.Popen) to produce output to consume. + configuration dict, but the given hook configuration is ignored. The given borgmatic runtime + directory is used to construct the destination path (used for the directory format), and the + given log prefix is used for any log entries. If this is a dry run, then don't actually restore + anything. Trigger the given active extract process (an instance of subprocess.Popen) to produce + output to consume. If the extract process is None, then restore the dump from the filesystem rather than from an extract stream. @@ -267,7 +277,9 @@ def restore_data_source_dump( all_databases = bool(data_source['name'] == 'all') dump_filename = dump.make_data_source_dump_filename( - make_dump_path(config), data_source['name'], data_source.get('hostname') + make_dump_path(borgmatic_runtime_directory), + data_source['name'], + data_source.get('hostname'), ) psql_command = tuple( shlex.quote(part) for part in shlex.split(data_source.get('psql_command') or 'psql') diff --git a/borgmatic/hooks/sqlite.py b/borgmatic/hooks/sqlite.py index 778a6226..f4ea6a72 100644 --- a/borgmatic/hooks/sqlite.py +++ b/borgmatic/hooks/sqlite.py @@ -9,15 +9,11 @@ from borgmatic.hooks import dump logger = logging.getLogger(__name__) -def make_dump_path(config, base_directory=None): # pragma: no cover +def make_dump_path(base_directory): # pragma: no cover ''' - Given a configuration dict and an optional base directory, make the corresponding dump path. If - a base directory isn't provided, use the borgmatic runtime directory. + Given a base directory, make the corresponding dump path. ''' - return dump.make_data_source_dump_path( - base_directory or borgmatic.config.paths.get_borgmatic_runtime_directory(config), - 'sqlite_databases', - ) + return dump.make_data_source_dump_path(base_directory, 'sqlite_databases') def use_streaming(databases, config, log_prefix): @@ -28,11 +24,11 @@ def use_streaming(databases, config, log_prefix): return any(databases) -def dump_data_sources(databases, config, log_prefix, dry_run): +def dump_data_sources(databases, config, log_prefix, borgmatic_runtime_directory, dry_run): ''' Dump the given SQLite databases to a named pipe. The databases are supplied as a sequence of - configuration dicts, as per the configuration schema. Use the given configuration dict to - construct the destination path and the given log prefix in any log entries. + configuration dicts, as per the configuration schema. Use the given borgmatic runtime directory + to construct the destination path and the given log prefix in any log entries. Return a sequence of subprocess.Popen instances for the dump processes ready to spew to a named pipe. But if this is a dry run, then don't actually dump anything and return an empty sequence. @@ -52,7 +48,7 @@ def dump_data_sources(databases, config, log_prefix, dry_run): f'{log_prefix}: No SQLite database at {database_path}; an empty database will be created and dumped' ) - dump_path = make_dump_path(config) + dump_path = make_dump_path(borgmatic_runtime_directory) dump_filename = dump.make_data_source_dump_filename(dump_path, database['name']) if os.path.exists(dump_filename): @@ -80,43 +76,55 @@ def dump_data_sources(databases, config, log_prefix, dry_run): return processes -def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma: no cover +def remove_data_source_dumps( + databases, config, log_prefix, borgmatic_runtime_directory, dry_run +): # pragma: no cover ''' - Remove the given SQLite database dumps from the filesystem. The databases are supplied as a - sequence of configuration dicts, as per the configuration schema. Use the given configuration - dict to construct the destination path and the given log prefix in any log entries. If this is a - dry run, then don't actually remove anything. + Remove all database dump files for this hook regardless of the given databases. Use the + borgmatic runtime directory to construct the destination path and the log prefix in any log + entries. If this is a dry run, then don't actually remove anything. ''' - dump.remove_data_source_dumps(make_dump_path(config), 'SQLite', log_prefix, dry_run) + dump.remove_data_source_dumps( + make_dump_path(borgmatic_runtime_directory), 'SQLite', log_prefix, dry_run + ) -def make_data_source_dump_patterns(databases, config, log_prefix, name=None): # pragma: no cover +def make_data_source_dump_patterns( + databases, config, log_prefix, borgmatic_runtime_directory, name=None +): # pragma: no cover ''' - Make a pattern that matches the given SQLite databases. The databases are supplied as a sequence - of configuration dicts, as per the configuration schema. + Given a sequence of configurations dicts, a configuration dict, a prefix to log with, the + borgmatic runtime directory, and a database name to match, return the corresponding glob + patterns to match the database dump in an archive. ''' borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config) return ( + dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'), dump.make_data_source_dump_filename( - make_dump_path(config, 'borgmatic'), name, hostname='*' + make_dump_path(borgmatic_runtime_directory), name, hostname='*' ), - dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*'), dump.make_data_source_dump_filename( - make_dump_path(config, borgmatic_source_directory), name, hostname='*' + make_dump_path(borgmatic_source_directory), name, hostname='*' ), ) def restore_data_source_dump( - hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params + hook_config, + config, + log_prefix, + data_source, + dry_run, + extract_process, + connection_params, + borgmatic_runtime_directory, ): ''' Restore a database from the given extract stream. The database is supplied as a data source - configuration dict, but the given hook configuration is ignored. The given configuration dict is - used to construct the destination path, and the given log prefix is used for any log entries. If - this is a dry run, then don't actually restore anything. Trigger the given active extract - process (an instance of subprocess.Popen) to produce output to consume. + configuration dict, but the given hook configuration is ignored. The given log prefix is used + for any log entries. If this is a dry run, then don't actually restore anything. Trigger the + given active extract process (an instance of subprocess.Popen) to produce output to consume. ''' dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else '' database_path = connection_params['restore_path'] or data_source.get( diff --git a/docs/how-to/backup-your-databases.md b/docs/how-to/backup-your-databases.md index f2162754..57eb68df 100644 --- a/docs/how-to/backup-your-databases.md +++ b/docs/how-to/backup-your-databases.md @@ -63,28 +63,9 @@ dump formats, which can't stream and therefore do consume temporary disk space. Additionally, prior to borgmatic 1.5.3, all database dumps consumed temporary disk space.) -New in version 1.9.0 To support -this, borgmatic creates temporary streaming database dumps within -`/run/user/$UID/borgmatic` by default (where `$UID` is the current user's ID). -To customize the `/run/user/$UID` portion of this path, set the -`user_runtime_directory` option in borgmatic's configuration. Alternatively, -set the `XDG_RUNTIME_DIR` environment variable (often already set to -`/run/user/$UID`). - -Prior to version 1.9.0 -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). - -New in version 1.9.1In addition -to `XDG_RUNTIME_DIR`, borgmatic also uses the `TMPDIR` or `TEMP` environment -variable if set. `TMPDIR` is available on macOS, while `TEMP` is often -available on other platforms. - -Also note that using a database hook implicitly enables the -`read_special` configuration option (even if it's disabled in your -configuration) to support this dump and restore streaming. See Limitations -below for more on this. +Also note that using a database hook implicitly enables the `read_special` +configuration option (even if it's disabled in your configuration) to support +this dump and restore streaming. See Limitations below for more on this. Here's a more involved example that connects to remote databases: @@ -132,6 +113,40 @@ additional customization of the options passed to database commands (when listing databases, restoring databases, etc.). +### Runtime directory + +New in version 1.9.0 To support +streaming database dumps to Borg, borgmatic uses a runtime directory for +temporary file storage, probing the following locations (in order) to find it: + + 1. The `user_runtime_directory` borgmatic configuration option. + 2. The `XDG_RUNTIME_DIR` environment variable, usually `/run/user/$UID` + (where `$UID` is the current user's ID), automatically set by PAM on Linux + for a user with a session. + 3. New in version 1.9.2The + `RUNTIME_DIRECTORY` environment variable, set by systemd if + `RuntimeDirectory=borgmatic` is added to borgmatic's systemd service file. + 4. New in version 1.9.1The + `TMPDIR` environment variable, set on macOS for a user with a session, + among other operating systems. + 5. New in version 1.9.1The + `TEMP` environment variable, set on various systems. + 6. New in version 1.9.2 + Hard-coded `/tmp`. Prior to + version 1.9.2This was instead hard-coded to `/run/user/$UID`. + +Regardless of the runtime directory selected, borgmatic stores its files +within a `borgmatic` subdirectory of the runtime directory. Additionally, in +the case of `TMPDIR`, `TEMP`, and the hard-coded `/tmp`, borgmatic creates a +randomly named subdirectory in an effort to reduce path collisions in shared +system temporary directories. + +Prior to version 1.9.0 +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). + + ### All databases If you want to dump all databases on a host, use `all` for the database name: diff --git a/docs/how-to/deal-with-very-large-backups.md b/docs/how-to/deal-with-very-large-backups.md index fbef5912..edbfa86a 100644 --- a/docs/how-to/deal-with-very-large-backups.md +++ b/docs/how-to/deal-with-very-large-backups.md @@ -240,6 +240,11 @@ this frequency logic, borgmatic records check timestamps within the portion of this path, set the `user_state_directory` configuration option. Alternatively, set the `XDG_STATE_HOME` environment variable. +New in version 1.9.2The +`STATE_DIRECTORY` environment variable also works for this purpose. It's set +by systemd if `StateDirectory=borgmatic` is added to borgmatic's systemd +service file. + Prior to version 1.9.0 borgmatic recorded check timestamps within the `~/.borgmatic` directory. At that time, the path was configurable by the `borgmatic_source_directory` diff --git a/docs/how-to/extract-a-backup.md b/docs/how-to/extract-a-backup.md index cc68824d..f6909110 100644 --- a/docs/how-to/extract-a-backup.md +++ b/docs/how-to/extract-a-backup.md @@ -156,12 +156,19 @@ borgmatic umount --mount-point /mnt ## Extract the configuration files used to create an archive -New in version 1.7.15 borgmatic -automatically stores all the configuration files used to create an archive -inside the archive itself. They are stored in the archive using their full -paths from the machine being backed up. This is useful in cases where you've -lost a configuration file or you want to see what configurations were used to -create a particular archive. +New in version 1.7.15 As part +of creating a backup archive, borgmatic automatically includes all of the +configuration files used when creating it, storing them inside the archive +itself with their full paths from the machine being backed up. This is useful +in cases where you've lost a configuration file or you want to see what +configurations were used to create a particular archive. + +To support this, borgmatic creates a manifest file that records the paths of +all the borgmatic configuration files stored within an archive. The file gets +written to borgmatic's runtime directory on disk and then stored within the +archive. See the [runtime directory +documentation](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#runtime-directory) +for how and where that happens. To extract the configuration files from an archive, use the `config bootstrap` action. For example: diff --git a/docs/how-to/inspect-your-backups.md b/docs/how-to/inspect-your-backups.md index 8b3676db..e28dc807 100644 --- a/docs/how-to/inspect-your-backups.md +++ b/docs/how-to/inspect-your-backups.md @@ -119,10 +119,10 @@ archive, regardless of the user who performs the backup. (Note that Borg doesn't store the leading `/`.) With Borg version 1.2 and -earlierDatabase dump files are stored at `/var/run/$UID/borgmatic` -(where `$UID` is the current user's ID) unless overridden by the -`user_runtime_directory` configuration option or the `XDG_STATE_HOME` -environment variable. +earlierDatabase dump files are stored at a path dependent on the +[runtime +directory](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#runtime-directory) +in use at the time the archive was created. Prior to borgmatic version 1.9.0Database dump files were instead stored at `~/.borgmatic` within diff --git a/sample/systemd/borgmatic.service b/sample/systemd/borgmatic.service index 7a96b834..564cbe99 100644 --- a/sample/systemd/borgmatic.service +++ b/sample/systemd/borgmatic.service @@ -9,6 +9,8 @@ Documentation=https://torsion.org/borgmatic/ [Service] Type=oneshot +RuntimeDirectory=borgmatic +StateDirectory=borgmatic # Load single encrypted credential. LoadCredentialEncrypted=borgmatic.pw diff --git a/tests/unit/actions/config/test_bootstrap.py b/tests/unit/actions/config/test_bootstrap.py index dceabe19..7a21e715 100644 --- a/tests/unit/actions/config/test_bootstrap.py +++ b/tests/unit/actions/config/test_bootstrap.py @@ -16,9 +16,6 @@ def test_get_config_paths_returns_list_of_config_paths(): flexmock(module.borgmatic.config.paths).should_receive( 'get_borgmatic_source_directory' ).and_return('/source') - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/runtime') flexmock(module).should_receive('make_bootstrap_config').and_return({}) bootstrap_arguments = flexmock( repository='repo', @@ -33,6 +30,12 @@ def test_get_config_paths_returns_list_of_config_paths(): dry_run=False, ) local_borg_version = flexmock() + flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return( + flexmock() + ) + flexmock(module.borgmatic.config.paths).should_receive( + 'make_runtime_directory_glob' + ).replace_with(lambda path: path) extract_process = flexmock( stdout=flexmock( read=lambda: '{"config_paths": ["/borgmatic/config.yaml"]}', @@ -47,13 +50,61 @@ def test_get_config_paths_returns_list_of_config_paths(): ) == ['/borgmatic/config.yaml'] +def test_get_config_paths_probes_for_manifest(): + flexmock(module.borgmatic.config.paths).should_receive( + 'get_borgmatic_source_directory' + ).and_return('/source') + flexmock(module).should_receive('make_bootstrap_config').and_return({}) + bootstrap_arguments = flexmock( + repository='repo', + archive='archive', + ssh_command=None, + local_path='borg7', + remote_path='borg8', + borgmatic_source_directory=None, + user_runtime_directory=None, + ) + global_arguments = flexmock( + dry_run=False, + ) + local_borg_version = flexmock() + borgmatic_runtime_directory = flexmock() + flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return( + borgmatic_runtime_directory, + ) + flexmock(module.borgmatic.config.paths).should_receive( + 'make_runtime_directory_glob' + ).replace_with(lambda path: path) + flexmock(module.os.path).should_receive('join').with_args( + 'borgmatic', 'bootstrap', 'manifest.json' + ).and_return('borgmatic/bootstrap/manifest.json').once() + flexmock(module.os.path).should_receive('join').with_args( + borgmatic_runtime_directory, 'bootstrap', 'manifest.json' + ).and_return('run/borgmatic/bootstrap/manifest.json').once() + flexmock(module.os.path).should_receive('join').with_args( + '/source', 'bootstrap', 'manifest.json' + ).and_return('/source/bootstrap/manifest.json').once() + manifest_missing_extract_process = flexmock( + stdout=flexmock(read=lambda: None), + ) + manifest_found_extract_process = flexmock( + stdout=flexmock( + read=lambda: '{"config_paths": ["/borgmatic/config.yaml"]}', + ), + ) + flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return( + manifest_missing_extract_process + ).and_return(manifest_missing_extract_process).and_return(manifest_found_extract_process) + + assert module.get_config_paths( + 'archive', bootstrap_arguments, global_arguments, local_borg_version + ) == ['/borgmatic/config.yaml'] + + def test_get_config_paths_translates_ssh_command_argument_to_config(): flexmock(module.borgmatic.config.paths).should_receive( 'get_borgmatic_source_directory' ).and_return('/source') - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/runtime') config = flexmock() flexmock(module).should_receive('make_bootstrap_config').and_return(config) bootstrap_arguments = flexmock( @@ -69,6 +120,12 @@ def test_get_config_paths_translates_ssh_command_argument_to_config(): dry_run=False, ) local_borg_version = flexmock() + flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return( + flexmock() + ) + flexmock(module.borgmatic.config.paths).should_receive( + 'make_runtime_directory_glob' + ).replace_with(lambda path: path) extract_process = flexmock( stdout=flexmock( read=lambda: '{"config_paths": ["/borgmatic/config.yaml"]}', @@ -96,9 +153,6 @@ def test_get_config_paths_with_missing_manifest_raises_value_error(): flexmock(module.borgmatic.config.paths).should_receive( 'get_borgmatic_source_directory' ).and_return('/source') - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/runtime') flexmock(module).should_receive('make_bootstrap_config').and_return({}) bootstrap_arguments = flexmock( repository='repo', @@ -113,6 +167,13 @@ def test_get_config_paths_with_missing_manifest_raises_value_error(): dry_run=False, ) local_borg_version = flexmock() + flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return( + flexmock() + ) + flexmock(module.borgmatic.config.paths).should_receive( + 'make_runtime_directory_glob' + ).replace_with(lambda path: path) + flexmock(module.os.path).should_receive('join').and_return('run/borgmatic') extract_process = flexmock(stdout=flexmock(read=lambda: '')) flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return( extract_process @@ -128,9 +189,6 @@ def test_get_config_paths_with_broken_json_raises_value_error(): flexmock(module.borgmatic.config.paths).should_receive( 'get_borgmatic_source_directory' ).and_return('/source') - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/runtime') flexmock(module).should_receive('make_bootstrap_config').and_return({}) bootstrap_arguments = flexmock( repository='repo', @@ -145,6 +203,12 @@ def test_get_config_paths_with_broken_json_raises_value_error(): dry_run=False, ) local_borg_version = flexmock() + flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return( + flexmock() + ) + flexmock(module.borgmatic.config.paths).should_receive( + 'make_runtime_directory_glob' + ).replace_with(lambda path: path) extract_process = flexmock( stdout=flexmock(read=lambda: '{"config_paths": ["/oops'), ) @@ -162,9 +226,6 @@ def test_get_config_paths_with_json_missing_key_raises_value_error(): flexmock(module.borgmatic.config.paths).should_receive( 'get_borgmatic_source_directory' ).and_return('/source') - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/runtime') flexmock(module).should_receive('make_bootstrap_config').and_return({}) bootstrap_arguments = flexmock( repository='repo', @@ -179,6 +240,12 @@ def test_get_config_paths_with_json_missing_key_raises_value_error(): dry_run=False, ) local_borg_version = flexmock() + flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return( + flexmock() + ) + flexmock(module.borgmatic.config.paths).should_receive( + 'make_runtime_directory_glob' + ).replace_with(lambda path: path) extract_process = flexmock( stdout=flexmock(read=lambda: '{}'), ) @@ -210,6 +277,12 @@ def test_run_bootstrap_does_not_raise(): dry_run=False, ) local_borg_version = flexmock() + flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return( + flexmock() + ) + flexmock(module.borgmatic.config.paths).should_receive( + 'make_runtime_directory_glob' + ).replace_with(lambda path: path) extract_process = flexmock( stdout=flexmock( read=lambda: '{"config_paths": ["borgmatic/config.yaml"]}', @@ -244,6 +317,12 @@ def test_run_bootstrap_translates_ssh_command_argument_to_config(): dry_run=False, ) local_borg_version = flexmock() + flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return( + flexmock() + ) + flexmock(module.borgmatic.config.paths).should_receive( + 'make_runtime_directory_glob' + ).replace_with(lambda path: path) extract_process = flexmock( stdout=flexmock( read=lambda: '{"config_paths": ["borgmatic/config.yaml"]}', diff --git a/tests/unit/actions/test_check.py b/tests/unit/actions/test_check.py index f8987733..e7299522 100644 --- a/tests/unit/actions/test_check.py +++ b/tests/unit/actions/test_check.py @@ -713,9 +713,6 @@ def test_collect_spot_check_archive_paths_excludes_directories(): flexmock(module.borgmatic.config.paths).should_receive( 'get_borgmatic_source_directory' ).and_return('/home/user/.borgmatic') - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/run/user/1001/borgmatic') flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return( ( 'f /etc/path', @@ -732,6 +729,7 @@ def test_collect_spot_check_archive_paths_excludes_directories(): global_arguments=flexmock(), local_path=flexmock(), remote_path=flexmock(), + borgmatic_runtime_directory='/run/user/1001/borgmatic', ) == ('/etc/path', '/etc/other') @@ -739,9 +737,6 @@ def test_collect_spot_check_archive_paths_excludes_file_in_borgmatic_runtime_dir flexmock(module.borgmatic.config.paths).should_receive( 'get_borgmatic_source_directory' ).and_return('/root/.borgmatic') - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/run/user/0/borgmatic') flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return( ( 'f /etc/path', @@ -757,6 +752,7 @@ def test_collect_spot_check_archive_paths_excludes_file_in_borgmatic_runtime_dir global_arguments=flexmock(), local_path=flexmock(), remote_path=flexmock(), + borgmatic_runtime_directory='/run/user/0/borgmatic', ) == ('/etc/path',) @@ -764,9 +760,6 @@ def test_collect_spot_check_archive_paths_excludes_file_in_borgmatic_source_dire flexmock(module.borgmatic.config.paths).should_receive( 'get_borgmatic_source_directory' ).and_return('/root/.borgmatic') - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/run/user/0/borgmatic') flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return( ( 'f /etc/path', @@ -782,6 +775,7 @@ def test_collect_spot_check_archive_paths_excludes_file_in_borgmatic_source_dire global_arguments=flexmock(), local_path=flexmock(), remote_path=flexmock(), + borgmatic_runtime_directory='/run/user/0/borgmatic', ) == ('/etc/path',) @@ -789,9 +783,6 @@ def test_collect_spot_check_archive_paths_excludes_file_in_borgmatic_runtime_dir flexmock(module.borgmatic.config.paths).should_receive( 'get_borgmatic_source_directory' ).and_return('/root.borgmatic') - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/run/user/0/borgmatic') flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return( ( 'f /etc/path', @@ -807,6 +798,7 @@ def test_collect_spot_check_archive_paths_excludes_file_in_borgmatic_runtime_dir global_arguments=flexmock(), local_path=flexmock(), remote_path=flexmock(), + borgmatic_runtime_directory='/run/user/0/borgmatic', ) == ('/etc/path',) @@ -887,7 +879,7 @@ def test_compare_spot_check_hashes_returns_paths_having_failing_hashes(): global_arguments=flexmock(), local_path=flexmock(), remote_path=flexmock(), - log_label='repo', + log_prefix='repo', source_paths=('/foo', '/bar', '/baz', '/quux'), ) == ('/bar',) @@ -928,7 +920,7 @@ def test_compare_spot_check_hashes_handles_data_sample_percentage_above_100(): global_arguments=flexmock(), local_path=flexmock(), remote_path=flexmock(), - log_label='repo', + log_prefix='repo', source_paths=('/foo', '/bar'), ) == ('/foo', '/bar') @@ -966,7 +958,7 @@ def test_compare_spot_check_hashes_uses_xxh64sum_command_option(): global_arguments=flexmock(), local_path=flexmock(), remote_path=flexmock(), - log_label='repo', + log_prefix='repo', source_paths=('/foo', '/bar', '/baz', '/quux'), ) == ('/bar',) @@ -1003,7 +995,7 @@ def test_compare_spot_check_hashes_considers_path_missing_from_archive_as_not_ma global_arguments=flexmock(), local_path=flexmock(), remote_path=flexmock(), - log_label='repo', + log_prefix='repo', source_paths=('/foo', '/bar', '/baz', '/quux'), ) == ('/bar',) @@ -1039,7 +1031,7 @@ def test_compare_spot_check_hashes_considers_non_existent_path_as_not_matching() global_arguments=flexmock(), local_path=flexmock(), remote_path=flexmock(), - log_label='repo', + log_prefix='repo', source_paths=('/foo', '/bar', '/baz', '/quux'), ) == ('/bar',) @@ -1086,7 +1078,7 @@ def test_compare_spot_check_hashes_with_too_many_paths_feeds_them_to_commands_in global_arguments=flexmock(), local_path=flexmock(), remote_path=flexmock(), - log_label='repo', + log_prefix='repo', source_paths=('/foo', '/bar', '/baz', '/quux'), ) == ('/quux',) @@ -1129,7 +1121,7 @@ def test_compare_spot_check_hashes_uses_working_directory_to_access_source_paths global_arguments=flexmock(), local_path=flexmock(), remote_path=flexmock(), - log_label='repo', + log_prefix='repo', source_paths=('foo', 'bar', 'baz', 'quux'), ) == ('bar',) @@ -1149,6 +1141,7 @@ def test_spot_check_without_spot_configuration_errors(): global_arguments=flexmock(), local_path=flexmock(), remote_path=flexmock(), + borgmatic_runtime_directory='/run/borgmatic', ) @@ -1161,6 +1154,7 @@ def test_spot_check_without_any_configuration_errors(): global_arguments=flexmock(), local_path=flexmock(), remote_path=flexmock(), + borgmatic_runtime_directory='/run/borgmatic', ) @@ -1181,6 +1175,7 @@ def test_spot_check_data_tolerance_percenatge_greater_than_data_sample_percentag global_arguments=flexmock(), local_path=flexmock(), remote_path=flexmock(), + borgmatic_runtime_directory='/run/borgmatic', ) @@ -1212,6 +1207,7 @@ def test_spot_check_with_count_delta_greater_than_count_tolerance_percentage_err global_arguments=flexmock(), local_path=flexmock(), remote_path=flexmock(), + borgmatic_runtime_directory='/run/borgmatic', ) @@ -1244,6 +1240,7 @@ def test_spot_check_with_failing_percentage_greater_than_data_tolerance_percenta global_arguments=flexmock(), local_path=flexmock(), remote_path=flexmock(), + borgmatic_runtime_directory='/run/borgmatic', ) @@ -1275,6 +1272,7 @@ def test_spot_check_with_high_enough_tolerances_does_not_raise(): global_arguments=flexmock(), local_path=flexmock(), remote_path=flexmock(), + borgmatic_runtime_directory='/run/borgmatic', ) @@ -1362,6 +1360,9 @@ def test_run_check_runs_configured_spot_check(): flexmock(module).should_receive('make_archives_check_id').and_return(None) flexmock(module).should_receive('filter_checks_on_frequency').and_return({'spot'}) flexmock(module.borgmatic.borg.check).should_receive('check_archives').never() + flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return( + flexmock() + ) flexmock(module.borgmatic.actions.check).should_receive('spot_check').once() flexmock(module).should_receive('make_check_time_path') flexmock(module).should_receive('write_check_time') diff --git a/tests/unit/actions/test_create.py b/tests/unit/actions/test_create.py index 8582abae..c8450897 100644 --- a/tests/unit/actions/test_create.py +++ b/tests/unit/actions/test_create.py @@ -8,6 +8,9 @@ from borgmatic.actions import create as module def test_run_create_executes_and_calls_hooks_for_configured_repository(): flexmock(module.logger).answer = lambda message: None flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never() + flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return( + flexmock() + ) flexmock(module.borgmatic.borg.create).should_receive('create_archive').once() flexmock(module).should_receive('create_borgmatic_manifest').once() flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2) @@ -44,6 +47,9 @@ def test_run_create_executes_and_calls_hooks_for_configured_repository(): def test_run_create_with_store_config_files_false_does_not_create_borgmatic_manifest(): flexmock(module.logger).answer = lambda message: None flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never() + flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return( + flexmock() + ) flexmock(module.borgmatic.borg.create).should_receive('create_archive').once() flexmock(module).should_receive('create_borgmatic_manifest').never() flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2) @@ -82,6 +88,9 @@ def test_run_create_runs_with_selected_repository(): flexmock(module.borgmatic.config.validate).should_receive( 'repositories_match' ).once().and_return(True) + flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return( + flexmock() + ) flexmock(module.borgmatic.borg.create).should_receive('create_archive').once() flexmock(module).should_receive('create_borgmatic_manifest').once() flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2) @@ -120,6 +129,7 @@ def test_run_create_bails_if_repository_does_not_match(): flexmock(module.borgmatic.config.validate).should_receive( 'repositories_match' ).once().and_return(False) + flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').never() flexmock(module.borgmatic.borg.create).should_receive('create_archive').never() flexmock(module).should_receive('create_borgmatic_manifest').never() create_arguments = flexmock( @@ -153,6 +163,9 @@ def test_run_create_produces_json(): flexmock(module.borgmatic.config.validate).should_receive( 'repositories_match' ).once().and_return(True) + flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return( + flexmock() + ) flexmock(module.borgmatic.borg.create).should_receive('create_archive').once().and_return( flexmock() ) @@ -191,18 +204,15 @@ def test_run_create_produces_json(): def test_create_borgmatic_manifest_creates_manifest_file(): - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/run/user/0/borgmatic') flexmock(module.os.path).should_receive('join').with_args( - '/run/user/0/borgmatic', 'bootstrap', 'manifest.json' - ).and_return('/run/user/0/borgmatic/bootstrap/manifest.json') + '/run/borgmatic', 'bootstrap', 'manifest.json' + ).and_return('/run/borgmatic/bootstrap/manifest.json') flexmock(module.os.path).should_receive('exists').and_return(False) flexmock(module.os).should_receive('makedirs').and_return(True) flexmock(module.importlib.metadata).should_receive('version').and_return('1.0.0') flexmock(sys.modules['builtins']).should_receive('open').with_args( - '/run/user/0/borgmatic/bootstrap/manifest.json', 'w' + '/run/borgmatic/bootstrap/manifest.json', 'w' ).and_return( flexmock( __enter__=lambda *args: flexmock(write=lambda *args: None, close=lambda *args: None), @@ -211,22 +221,19 @@ def test_create_borgmatic_manifest_creates_manifest_file(): ) flexmock(module.json).should_receive('dump').and_return(True).once() - module.create_borgmatic_manifest({}, 'test.yaml', False) + module.create_borgmatic_manifest({}, 'test.yaml', '/run/borgmatic', False) def test_create_borgmatic_manifest_creates_manifest_file_with_custom_borgmatic_runtime_directory(): - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/borgmatic') flexmock(module.os.path).should_receive('join').with_args( - '/borgmatic', 'bootstrap', 'manifest.json' - ).and_return('/borgmatic/bootstrap/manifest.json') + '/run/borgmatic', 'bootstrap', 'manifest.json' + ).and_return('/run/borgmatic/bootstrap/manifest.json') flexmock(module.os.path).should_receive('exists').and_return(False) flexmock(module.os).should_receive('makedirs').and_return(True) flexmock(module.importlib.metadata).should_receive('version').and_return('1.0.0') flexmock(sys.modules['builtins']).should_receive('open').with_args( - '/borgmatic/bootstrap/manifest.json', 'w' + '/run/borgmatic/bootstrap/manifest.json', 'w' ).and_return( flexmock( __enter__=lambda *args: flexmock(write=lambda *args: None, close=lambda *args: None), @@ -236,9 +243,11 @@ def test_create_borgmatic_manifest_creates_manifest_file_with_custom_borgmatic_r flexmock(module.json).should_receive('dump').and_return(True).once() module.create_borgmatic_manifest( - {'borgmatic_runtime_directory': '/borgmatic'}, 'test.yaml', False + {'borgmatic_runtime_directory': '/borgmatic'}, 'test.yaml', '/run/borgmatic', False ) def test_create_borgmatic_manifest_does_not_create_manifest_file_on_dry_run(): - module.create_borgmatic_manifest({}, 'test.yaml', True) + flexmock(module.json).should_receive('dump').never() + + module.create_borgmatic_manifest({}, 'test.yaml', '/run/borgmatic', True) diff --git a/tests/unit/actions/test_restore.py b/tests/unit/actions/test_restore.py index d9e3d9d6..e12c392a 100644 --- a/tests/unit/actions/test_restore.py +++ b/tests/unit/actions/test_restore.py @@ -87,11 +87,8 @@ def test_strip_path_prefix_from_extracted_dump_destination_renames_first_matchin def test_restore_single_data_source_extracts_and_restores_single_file_dump(): flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').with_args( - 'make_data_source_dump_patterns', object, object, object, object + 'make_data_source_dump_patterns', object, object, object, object, object ).and_return({'postgresql': flexmock()}) - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/run/user/0/borgmatic') flexmock(module.tempfile).should_receive('mkdtemp').never() flexmock(module.borgmatic.hooks.dump).should_receive( 'convert_glob_patterns_to_borg_pattern' @@ -110,6 +107,7 @@ def test_restore_single_data_source_extracts_and_restores_single_file_dump(): dry_run=object, extract_process=object, connection_params=object, + borgmatic_runtime_directory=object, ).once() module.restore_single_data_source( @@ -123,16 +121,14 @@ def test_restore_single_data_source_extracts_and_restores_single_file_dump(): hook_name='postgresql', data_source={'name': 'test', 'format': 'plain'}, connection_params=flexmock(), + borgmatic_runtime_directory='/run/borgmatic', ) def test_restore_single_data_source_extracts_and_restores_directory_dump(): flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').with_args( - 'make_data_source_dump_patterns', object, object, object, object + 'make_data_source_dump_patterns', object, object, object, object, object ).and_return({'postgresql': flexmock()}) - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/run/user/0/borgmatic') flexmock(module.tempfile).should_receive('mkdtemp').once().and_return( '/run/user/0/borgmatic/tmp1234' ) @@ -153,6 +149,7 @@ def test_restore_single_data_source_extracts_and_restores_directory_dump(): dry_run=object, extract_process=object, connection_params=object, + borgmatic_runtime_directory='/run/borgmatic', ).once() module.restore_single_data_source( @@ -166,16 +163,14 @@ def test_restore_single_data_source_extracts_and_restores_directory_dump(): hook_name='postgresql', data_source={'name': 'test', 'format': 'directory'}, connection_params=flexmock(), + borgmatic_runtime_directory='/run/borgmatic', ) def test_restore_single_data_source_with_directory_dump_error_cleans_up_temporary_directory(): flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').with_args( - 'make_data_source_dump_patterns', object, object, object, object + 'make_data_source_dump_patterns', object, object, object, object, object ).and_return({'postgresql': flexmock()}) - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/run/user/0/borgmatic') flexmock(module.tempfile).should_receive('mkdtemp').once().and_return( '/run/user/0/borgmatic/tmp1234' ) @@ -196,6 +191,7 @@ def test_restore_single_data_source_with_directory_dump_error_cleans_up_temporar dry_run=object, extract_process=object, connection_params=object, + borgmatic_runtime_directory='/run/user/0/borgmatic/tmp1234', ).never() with pytest.raises(ValueError): @@ -210,19 +206,15 @@ def test_restore_single_data_source_with_directory_dump_error_cleans_up_temporar hook_name='postgresql', data_source={'name': 'test', 'format': 'directory'}, connection_params=flexmock(), + borgmatic_runtime_directory='/run/borgmatic', ) def test_restore_single_data_source_with_directory_dump_and_dry_run_skips_directory_move_and_cleanup(): flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').with_args( - 'make_data_source_dump_patterns', object, object, object, object + 'make_data_source_dump_patterns', object, object, object, object, object ).and_return({'postgresql': flexmock()}) - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/run/user/0/borgmatic') - flexmock(module.tempfile).should_receive('mkdtemp').once().and_return( - '/run/user/0/borgmatic/tmp1234' - ) + flexmock(module.tempfile).should_receive('mkdtemp').once().and_return('/run/borgmatic/tmp1234') flexmock(module.borgmatic.hooks.dump).should_receive( 'convert_glob_patterns_to_borg_pattern' ).and_return(flexmock()) @@ -240,6 +232,7 @@ def test_restore_single_data_source_with_directory_dump_and_dry_run_skips_direct dry_run=object, extract_process=object, connection_params=object, + borgmatic_runtime_directory='/run/borgmatic', ).once() module.restore_single_data_source( @@ -253,6 +246,7 @@ def test_restore_single_data_source_with_directory_dump_and_dry_run_skips_direct hook_name='postgresql', data_source={'name': 'test', 'format': 'directory'}, connection_params=flexmock(), + borgmatic_runtime_directory='/run/borgmatic', ) @@ -260,9 +254,6 @@ def test_collect_archive_data_source_names_parses_archive_paths(): flexmock(module.borgmatic.config.paths).should_receive( 'get_borgmatic_source_directory' ).and_return('/root/.borgmatic') - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/run/user/0/borgmatic') flexmock(module.borgmatic.hooks.dump).should_receive('make_data_source_dump_path').and_return( '' ) @@ -282,6 +273,7 @@ def test_collect_archive_data_source_names_parses_archive_paths(): global_arguments=flexmock(log_json=False), local_path=flexmock(), remote_path=flexmock(), + borgmatic_runtime_directory='/run/borgmatic', ) assert archive_data_source_names == { @@ -294,9 +286,6 @@ def test_collect_archive_data_source_names_parses_archive_paths_with_different_b flexmock(module.borgmatic.config.paths).should_receive( 'get_borgmatic_source_directory' ).and_return('/root/.borgmatic') - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/run/user/0/borgmatic') flexmock(module.borgmatic.hooks.dump).should_receive('make_data_source_dump_path').and_return( '' ) @@ -317,6 +306,7 @@ def test_collect_archive_data_source_names_parses_archive_paths_with_different_b global_arguments=flexmock(log_json=False), local_path=flexmock(), remote_path=flexmock(), + borgmatic_runtime_directory='/run/borgmatic', ) assert archive_data_source_names == { @@ -329,9 +319,6 @@ def test_collect_archive_data_source_names_parses_directory_format_archive_paths flexmock(module.borgmatic.config.paths).should_receive( 'get_borgmatic_source_directory' ).and_return('/root/.borgmatic') - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/run/user/0/borgmatic') flexmock(module.borgmatic.hooks.dump).should_receive('make_data_source_dump_path').and_return( '' ) @@ -350,6 +337,7 @@ def test_collect_archive_data_source_names_parses_directory_format_archive_paths global_arguments=flexmock(log_json=False), local_path=flexmock(), remote_path=flexmock(), + borgmatic_runtime_directory='/run/borgmatic', ) assert archive_data_source_names == { @@ -361,9 +349,6 @@ def test_collect_archive_data_source_names_skips_bad_archive_paths(): flexmock(module.borgmatic.config.paths).should_receive( 'get_borgmatic_source_directory' ).and_return('/root/.borgmatic') - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/run/user/0/borgmatic') flexmock(module.borgmatic.hooks.dump).should_receive('make_data_source_dump_path').and_return( '' ) @@ -384,6 +369,7 @@ def test_collect_archive_data_source_names_skips_bad_archive_paths(): global_arguments=flexmock(log_json=False), local_path=flexmock(), remote_path=flexmock(), + borgmatic_runtime_directory='/run/borgmatic', ) assert archive_data_source_names == { @@ -481,6 +467,13 @@ def test_run_restore_restores_each_data_source(): } flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True) + borgmatic_runtime_directory = flexmock() + flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return( + borgmatic_runtime_directory + ) + flexmock(module.borgmatic.config.paths).should_receive( + 'make_runtime_directory_glob' + ).replace_with(lambda path: path) flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks_even_if_unconfigured') flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return( flexmock() @@ -501,6 +494,7 @@ def test_run_restore_restores_each_data_source(): hook_name='postgresql_databases', data_source={'name': 'foo', 'schemas': None}, connection_params=object, + borgmatic_runtime_directory=borgmatic_runtime_directory, ).once() flexmock(module).should_receive('restore_single_data_source').with_args( repository=object, @@ -513,6 +507,7 @@ def test_run_restore_restores_each_data_source(): hook_name='postgresql_databases', data_source={'name': 'bar', 'schemas': None}, connection_params=object, + borgmatic_runtime_directory=borgmatic_runtime_directory, ).once() flexmock(module).should_receive('ensure_data_sources_found') @@ -541,6 +536,12 @@ def test_run_restore_bails_for_non_matching_repository(): flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return( False ) + flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return( + flexmock() + ) + flexmock(module.borgmatic.config.paths).should_receive( + 'make_runtime_directory_glob' + ).replace_with(lambda path: path) flexmock(module.borgmatic.hooks.dispatch).should_receive( 'call_hooks_even_if_unconfigured' ).never() @@ -563,6 +564,13 @@ def test_run_restore_restores_data_source_configured_with_all_name(): } flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True) + borgmatic_runtime_directory = flexmock() + flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return( + borgmatic_runtime_directory + ) + flexmock(module.borgmatic.config.paths).should_receive( + 'make_runtime_directory_glob' + ).replace_with(lambda path: path) flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks_even_if_unconfigured') flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return( flexmock() @@ -599,6 +607,7 @@ def test_run_restore_restores_data_source_configured_with_all_name(): hook_name='postgresql_databases', data_source={'name': 'foo', 'schemas': None}, connection_params=object, + borgmatic_runtime_directory=borgmatic_runtime_directory, ).once() flexmock(module).should_receive('restore_single_data_source').with_args( repository=object, @@ -611,6 +620,7 @@ def test_run_restore_restores_data_source_configured_with_all_name(): hook_name='postgresql_databases', data_source={'name': 'bar', 'schemas': None}, connection_params=object, + borgmatic_runtime_directory=borgmatic_runtime_directory, ).once() flexmock(module).should_receive('ensure_data_sources_found') @@ -641,6 +651,13 @@ def test_run_restore_skips_missing_data_source(): } flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True) + borgmatic_runtime_directory = flexmock() + flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return( + borgmatic_runtime_directory + ) + flexmock(module.borgmatic.config.paths).should_receive( + 'make_runtime_directory_glob' + ).replace_with(lambda path: path) flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks_even_if_unconfigured') flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return( flexmock() @@ -677,6 +694,7 @@ def test_run_restore_skips_missing_data_source(): hook_name='postgresql_databases', data_source={'name': 'foo', 'schemas': None}, connection_params=object, + borgmatic_runtime_directory=borgmatic_runtime_directory, ).once() flexmock(module).should_receive('restore_single_data_source').with_args( repository=object, @@ -689,6 +707,7 @@ def test_run_restore_skips_missing_data_source(): hook_name='postgresql_databases', data_source={'name': 'bar', 'schemas': None}, connection_params=object, + borgmatic_runtime_directory=borgmatic_runtime_directory, ).never() flexmock(module).should_receive('ensure_data_sources_found') @@ -720,6 +739,13 @@ def test_run_restore_restores_data_sources_from_different_hooks(): } flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True) + borgmatic_runtime_directory = flexmock() + flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return( + borgmatic_runtime_directory + ) + flexmock(module.borgmatic.config.paths).should_receive( + 'make_runtime_directory_glob' + ).replace_with(lambda path: path) flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks_even_if_unconfigured') flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return( flexmock() @@ -749,6 +775,7 @@ def test_run_restore_restores_data_sources_from_different_hooks(): hook_name='postgresql_databases', data_source={'name': 'foo', 'schemas': None}, connection_params=object, + borgmatic_runtime_directory=borgmatic_runtime_directory, ).once() flexmock(module).should_receive('restore_single_data_source').with_args( repository=object, @@ -761,6 +788,7 @@ def test_run_restore_restores_data_sources_from_different_hooks(): hook_name='mysql_databases', data_source={'name': 'bar', 'schemas': None}, connection_params=object, + borgmatic_runtime_directory=borgmatic_runtime_directory, ).once() flexmock(module).should_receive('ensure_data_sources_found') diff --git a/tests/unit/borg/test_create.py b/tests/unit/borg/test_create.py index f24707a5..b1f8b509 100644 --- a/tests/unit/borg/test_create.py +++ b/tests/unit/borg/test_create.py @@ -1453,9 +1453,6 @@ def test_create_archive_calls_borg_with_parameters(): flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels') flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER flexmock(module).should_receive('expand_directories').and_return(()) - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/var/run/0/borgmatic') flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([]) flexmock(module).should_receive('make_base_create_command').and_return( (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock()) @@ -1483,6 +1480,7 @@ def test_create_archive_calls_borg_with_parameters(): config_paths=['/tmp/test.yaml'], local_borg_version='1.2.3', global_arguments=flexmock(log_json=False), + borgmatic_runtime_directory='/borgmatic/run', ) @@ -1490,9 +1488,6 @@ def test_create_archive_calls_borg_with_environment(): flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels') flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER flexmock(module).should_receive('expand_directories').and_return(()) - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/var/run/0/borgmatic') flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([]) flexmock(module).should_receive('make_base_create_command').and_return( (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock()) @@ -1521,6 +1516,7 @@ def test_create_archive_calls_borg_with_environment(): config_paths=['/tmp/test.yaml'], local_borg_version='1.2.3', global_arguments=flexmock(log_json=False), + borgmatic_runtime_directory='/borgmatic/run', ) @@ -1528,9 +1524,6 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter(): flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels') flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER flexmock(module).should_receive('expand_directories').and_return(()) - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/var/run/0/borgmatic') flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([]) flexmock(module).should_receive('make_base_create_command').and_return( (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock()) @@ -1559,6 +1552,7 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter(): config_paths=['/tmp/test.yaml'], local_borg_version='1.2.3', global_arguments=flexmock(log_json=False), + borgmatic_runtime_directory='/borgmatic/run', ) @@ -1566,9 +1560,6 @@ def test_create_archive_with_log_info_and_json_suppresses_most_borg_output(): flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels') flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER flexmock(module).should_receive('expand_directories').and_return(()) - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/var/run/0/borgmatic') flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([]) flexmock(module).should_receive('make_base_create_command').and_return( (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock()) @@ -1595,6 +1586,7 @@ def test_create_archive_with_log_info_and_json_suppresses_most_borg_output(): config_paths=['/tmp/test.yaml'], local_borg_version='1.2.3', global_arguments=flexmock(log_json=False), + borgmatic_runtime_directory='/borgmatic/run', json=True, ) @@ -1603,9 +1595,6 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter(): flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels') flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER flexmock(module).should_receive('expand_directories').and_return(()) - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/var/run/0/borgmatic') flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([]) flexmock(module).should_receive('make_base_create_command').and_return( (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock()) @@ -1634,6 +1623,7 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter(): config_paths=['/tmp/test.yaml'], local_borg_version='1.2.3', global_arguments=flexmock(log_json=False), + borgmatic_runtime_directory='/borgmatic/run', ) @@ -1641,9 +1631,6 @@ def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output(): flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels') flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER flexmock(module).should_receive('expand_directories').and_return(()) - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/var/run/0/borgmatic') flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([]) flexmock(module).should_receive('make_base_create_command').and_return( (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock()) @@ -1670,6 +1657,7 @@ def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output(): config_paths=['/tmp/test.yaml'], local_borg_version='1.2.3', global_arguments=flexmock(log_json=False), + borgmatic_runtime_directory='/borgmatic/run', json=True, ) @@ -1680,9 +1668,6 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats(): flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels') flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER flexmock(module).should_receive('expand_directories').and_return(()) - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/var/run/0/borgmatic') flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([]) flexmock(module).should_receive('make_base_create_command').and_return( (('borg', 'create', '--dry-run'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock()) @@ -1711,6 +1696,7 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats(): config_paths=['/tmp/test.yaml'], local_borg_version='1.2.3', global_arguments=flexmock(log_json=False), + borgmatic_runtime_directory='/borgmatic/run', stats=True, ) @@ -1719,9 +1705,6 @@ def test_create_archive_with_working_directory_calls_borg_with_working_directory flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels') flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER flexmock(module).should_receive('expand_directories').and_return(()) - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/var/run/0/borgmatic') flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([]) flexmock(module).should_receive('make_base_create_command').and_return( (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock()) @@ -1752,6 +1735,7 @@ def test_create_archive_with_working_directory_calls_borg_with_working_directory config_paths=['/tmp/test.yaml'], local_borg_version='1.2.3', global_arguments=flexmock(log_json=False), + borgmatic_runtime_directory='/borgmatic/run', ) @@ -1759,9 +1743,6 @@ def test_create_archive_with_exit_codes_calls_borg_using_them(): flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels') flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER flexmock(module).should_receive('expand_directories').and_return(()) - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/var/run/0/borgmatic') flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([]) flexmock(module).should_receive('make_base_create_command').and_return( (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock()) @@ -1791,6 +1772,7 @@ def test_create_archive_with_exit_codes_calls_borg_using_them(): config_paths=['/tmp/test.yaml'], local_borg_version='1.2.3', global_arguments=flexmock(log_json=False), + borgmatic_runtime_directory='/borgmatic/run', ) @@ -1798,9 +1780,6 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_answer_ou flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels') flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER flexmock(module).should_receive('expand_directories').and_return(()) - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/var/run/0/borgmatic') flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([]) flexmock(module).should_receive('make_base_create_command').and_return( (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock()) @@ -1828,6 +1807,7 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_answer_ou config_paths=['/tmp/test.yaml'], local_borg_version='1.2.3', global_arguments=flexmock(log_json=False), + borgmatic_runtime_directory='/borgmatic/run', stats=True, ) @@ -1836,9 +1816,6 @@ def test_create_archive_with_files_calls_borg_with_answer_output_log_level(): flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels') flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER flexmock(module).should_receive('expand_directories').and_return(()) - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/var/run/0/borgmatic') flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([]) flexmock(module).should_receive('make_base_create_command').and_return( ( @@ -1871,6 +1848,7 @@ def test_create_archive_with_files_calls_borg_with_answer_output_log_level(): config_paths=['/tmp/test.yaml'], local_borg_version='1.2.3', global_arguments=flexmock(log_json=False), + borgmatic_runtime_directory='/borgmatic/run', list_files=True, ) @@ -1879,9 +1857,6 @@ def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_para flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels') flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER flexmock(module).should_receive('expand_directories').and_return(()) - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/var/run/0/borgmatic') flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([]) flexmock(module).should_receive('make_base_create_command').and_return( (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock()) @@ -1910,6 +1885,7 @@ def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_para config_paths=['/tmp/test.yaml'], local_borg_version='1.2.3', global_arguments=flexmock(log_json=False), + borgmatic_runtime_directory='/borgmatic/run', progress=True, ) @@ -1918,9 +1894,6 @@ def test_create_archive_with_progress_calls_borg_with_progress_parameter(): flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels') flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER flexmock(module).should_receive('expand_directories').and_return(()) - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/var/run/0/borgmatic') flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([]) flexmock(module).should_receive('make_base_create_command').and_return( (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock()) @@ -1948,6 +1921,7 @@ def test_create_archive_with_progress_calls_borg_with_progress_parameter(): config_paths=['/tmp/test.yaml'], local_borg_version='1.2.3', global_arguments=flexmock(log_json=False), + borgmatic_runtime_directory='/borgmatic/run', progress=True, ) @@ -1957,9 +1931,6 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER processes = flexmock() flexmock(module).should_receive('expand_directories').and_return(()) - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/var/run/0/borgmatic') flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([]) flexmock(module).should_receive('make_base_create_command').and_return( ( @@ -2009,6 +1980,7 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr config_paths=['/tmp/test.yaml'], local_borg_version='1.2.3', global_arguments=flexmock(log_json=False), + borgmatic_runtime_directory='/borgmatic/run', progress=True, stream_processes=processes, ) @@ -2018,9 +1990,6 @@ def test_create_archive_with_json_calls_borg_with_json_flag(): flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels') flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER flexmock(module).should_receive('expand_directories').and_return(()) - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/var/run/0/borgmatic') flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([]) flexmock(module).should_receive('make_base_create_command').and_return( (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock()) @@ -2046,6 +2015,7 @@ def test_create_archive_with_json_calls_borg_with_json_flag(): config_paths=['/tmp/test.yaml'], local_borg_version='1.2.3', global_arguments=flexmock(log_json=False), + borgmatic_runtime_directory='/borgmatic/run', json=True, ) @@ -2056,9 +2026,6 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_flag(): flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels') flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER flexmock(module).should_receive('expand_directories').and_return(()) - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/var/run/0/borgmatic') flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([]) flexmock(module).should_receive('make_base_create_command').and_return( (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock()) @@ -2084,6 +2051,7 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_flag(): config_paths=['/tmp/test.yaml'], local_borg_version='1.2.3', global_arguments=flexmock(log_json=False), + borgmatic_runtime_directory='/borgmatic/run', json=True, stats=True, ) @@ -2095,9 +2063,6 @@ def test_create_archive_calls_borg_with_working_directory(): flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels') flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER flexmock(module).should_receive('expand_directories').and_return(()) - flexmock(module.borgmatic.config.paths).should_receive( - 'get_borgmatic_runtime_directory' - ).and_return('/var/run/0/borgmatic') flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([]) flexmock(module).should_receive('make_base_create_command').and_return( (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock()) @@ -2128,6 +2093,7 @@ def test_create_archive_calls_borg_with_working_directory(): config_paths=['/tmp/test.yaml'], local_borg_version='1.2.3', global_arguments=flexmock(log_json=False), + borgmatic_runtime_directory='/borgmatic/run', ) diff --git a/tests/unit/config/test_paths.py b/tests/unit/config/test_paths.py index deeac41b..36a8fb50 100644 --- a/tests/unit/config/test_paths.py +++ b/tests/unit/config/test_paths.py @@ -1,3 +1,4 @@ +import pytest from flexmock import flexmock from borgmatic.config import paths as module @@ -33,53 +34,141 @@ def test_get_borgmatic_source_directory_without_config_option_uses_default(): assert module.get_borgmatic_source_directory({}) == '~/.borgmatic' -def test_get_borgmatic_runtime_directory_uses_config_option(): +def test_runtime_directory_uses_config_option(): flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path) + flexmock(module.os).should_receive('makedirs') + config = {'user_runtime_directory': '/run', 'borgmatic_source_directory': '/nope'} - assert ( - module.get_borgmatic_runtime_directory( - {'user_runtime_directory': '/tmp', 'borgmatic_source_directory': '/nope'} - ) - == '/tmp/./borgmatic' - ) + with module.Runtime_directory(config, 'prefix') as borgmatic_runtime_directory: + assert borgmatic_runtime_directory == '/run/./borgmatic' -def test_get_borgmatic_runtime_directory_falls_back_to_linux_environment_variable(): +def test_runtime_directory_uses_config_option_without_adding_duplicate_borgmatic_subdirectory(): + flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path) + flexmock(module.os).should_receive('makedirs') + config = {'user_runtime_directory': '/run/borgmatic', 'borgmatic_source_directory': '/nope'} + + with module.Runtime_directory(config, 'prefix') as borgmatic_runtime_directory: + assert borgmatic_runtime_directory == '/run/./borgmatic' + + +def test_runtime_directory_falls_back_to_xdg_runtime_dir(): flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path) flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return( - '/tmp' + '/run' ) + flexmock(module.os).should_receive('makedirs') - assert module.get_borgmatic_runtime_directory({}) == '/tmp/./borgmatic' + with module.Runtime_directory({}, 'prefix') as borgmatic_runtime_directory: + assert borgmatic_runtime_directory == '/run/./borgmatic' -def test_get_borgmatic_runtime_directory_falls_back_to_macos_environment_variable(): +def test_runtime_directory_falls_back_to_xdg_runtime_dir_without_adding_duplicate_borgmatic_subdirectory(): + flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path) + flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return( + '/run/borgmatic' + ) + flexmock(module.os).should_receive('makedirs') + + with module.Runtime_directory({}, 'prefix') as borgmatic_runtime_directory: + assert borgmatic_runtime_directory == '/run/./borgmatic' + + +def test_runtime_directory_falls_back_to_runtime_directory(): flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path) flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None) - flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return('/tmp') + flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return( + '/run' + ) + flexmock(module.os).should_receive('makedirs') - assert module.get_borgmatic_runtime_directory({}) == '/tmp/./borgmatic' + with module.Runtime_directory({}, 'prefix') as borgmatic_runtime_directory: + assert borgmatic_runtime_directory == '/run/./borgmatic' -def test_get_borgmatic_runtime_directory_falls_back_to_other_environment_variable(): +def test_runtime_directory_falls_back_to_runtime_directory_without_adding_duplicate_borgmatic_subdirectory(): flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path) flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None) + flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return( + '/run/borgmatic' + ) + flexmock(module.os).should_receive('makedirs') + + with module.Runtime_directory({}, 'prefix') as borgmatic_runtime_directory: + assert borgmatic_runtime_directory == '/run/./borgmatic' + + +def test_runtime_directory_falls_back_to_tmpdir_and_adds_temporary_subdirectory_that_get_cleaned_up(): + flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path) + flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None) + flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return( + None + ) + flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return('/run') + temporary_directory = flexmock(name='/run/borgmatic-1234') + temporary_directory.should_receive('cleanup').once() + flexmock(module.tempfile).should_receive('TemporaryDirectory').with_args( + prefix='borgmatic-', dir='/run' + ).and_return(temporary_directory) + flexmock(module.os).should_receive('makedirs') + + with module.Runtime_directory({}, 'prefix') as borgmatic_runtime_directory: + assert borgmatic_runtime_directory == '/run/borgmatic-1234/./borgmatic' + + +def test_runtime_directory_falls_back_to_temp_and_adds_temporary_subdirectory_that_get_cleaned_up(): + flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path) + flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None) + flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return( + None + ) flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None) - flexmock(module.os.environ).should_receive('get').with_args('TEMP').and_return('/tmp') + flexmock(module.os.environ).should_receive('get').with_args('TEMP').and_return('/run') + temporary_directory = flexmock(name='/run/borgmatic-1234') + temporary_directory.should_receive('cleanup').once() + flexmock(module.tempfile).should_receive('TemporaryDirectory').with_args( + prefix='borgmatic-', dir='/run' + ).and_return(temporary_directory) + flexmock(module.os).should_receive('makedirs') - assert module.get_borgmatic_runtime_directory({}) == '/tmp/./borgmatic' + with module.Runtime_directory({}, 'prefix') as borgmatic_runtime_directory: + assert borgmatic_runtime_directory == '/run/borgmatic-1234/./borgmatic' -def test_get_borgmatic_runtime_directory_defaults_to_hard_coded_path(): +def test_runtime_directory_falls_back_to_hard_coded_tmp_path_and_adds_temporary_subdirectory_that_get_cleaned_up(): flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path) - flexmock(module.os.environ).should_receive('get').and_return('/run/user/0') - flexmock(module.os).should_receive('getuid').and_return(0) + flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None) + flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return( + None + ) + flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None) + flexmock(module.os.environ).should_receive('get').with_args('TEMP').and_return(None) + temporary_directory = flexmock(name='/tmp/borgmatic-1234') + temporary_directory.should_receive('cleanup').once() + flexmock(module.tempfile).should_receive('TemporaryDirectory').with_args( + prefix='borgmatic-', dir='/tmp' + ).and_return(temporary_directory) + flexmock(module.os).should_receive('makedirs') - assert module.get_borgmatic_runtime_directory({}) == '/run/user/0/./borgmatic' + with module.Runtime_directory({}, 'prefix') as borgmatic_runtime_directory: + assert borgmatic_runtime_directory == '/tmp/borgmatic-1234/./borgmatic' + + +@pytest.mark.parametrize( + 'borgmatic_runtime_directory,expected_glob', + ( + ('/foo/bar/baz/./borgmatic', 'foo/bar/baz/borgmatic'), + ('/foo/borgmatic/baz/./borgmatic', 'foo/borgmatic/baz/borgmatic'), + ('/foo/borgmatic-jti8idds/./borgmatic', 'foo/*/borgmatic'), + ), +) +def test_make_runtime_directory_glob(borgmatic_runtime_directory, expected_glob): + assert module.make_runtime_directory_glob(borgmatic_runtime_directory) == expected_glob def test_get_borgmatic_state_directory_uses_config_option(): flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path) + flexmock(module.os.environ).should_receive('get').never() assert ( module.get_borgmatic_state_directory( @@ -89,16 +178,24 @@ def test_get_borgmatic_state_directory_uses_config_option(): ) -def test_get_borgmatic_state_directory_falls_back_to_environment_variable(): +def test_get_borgmatic_state_directory_falls_back_to_xdg_state_home(): flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path) - flexmock(module.os.environ).should_receive('get').with_args( - 'XDG_STATE_HOME', object - ).and_return('/tmp') + flexmock(module.os.environ).should_receive('get').with_args('XDG_STATE_HOME').and_return('/tmp') + + assert module.get_borgmatic_state_directory({}) == '/tmp/borgmatic' + + +def test_get_borgmatic_state_directory_falls_back_to_state_directory(): + flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path) + flexmock(module.os.environ).should_receive('get').with_args('XDG_STATE_HOME').and_return(None) + flexmock(module.os.environ).should_receive('get').with_args('STATE_DIRECTORY').and_return( + '/tmp' + ) assert module.get_borgmatic_state_directory({}) == '/tmp/borgmatic' def test_get_borgmatic_state_directory_defaults_to_hard_coded_path(): flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path) - flexmock(module.os.environ).should_receive('get').and_return('/root/.local/state') - assert module.get_borgmatic_state_directory({}) == '/root/.local/state/borgmatic' + flexmock(module.os.environ).should_receive('get').and_return(None) + assert module.get_borgmatic_state_directory({}) == '~/.local/state/borgmatic' diff --git a/tests/unit/hooks/test_mariadb.py b/tests/unit/hooks/test_mariadb.py index 8034afed..fb5612be 100644 --- a/tests/unit/hooks/test_mariadb.py +++ b/tests/unit/hooks/test_mariadb.py @@ -73,7 +73,12 @@ def test_dump_data_sources_dumps_each_database(): dry_run_label=object, ).and_return(process).once() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes + assert ( + module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) + == processes + ) def test_dump_data_sources_dumps_with_password(): @@ -94,7 +99,9 @@ def test_dump_data_sources_dumps_with_password(): dry_run_label=object, ).and_return(process).once() - assert module.dump_data_sources([database], {}, 'test.yaml', dry_run=False) == [process] + assert module.dump_data_sources( + [database], {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) == [process] def test_dump_data_sources_dumps_all_databases_at_once(): @@ -112,7 +119,9 @@ def test_dump_data_sources_dumps_all_databases_at_once(): dry_run_label=object, ).and_return(process).once() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process] + assert module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) == [process] def test_dump_data_sources_dumps_all_databases_separately_when_format_configured(): @@ -132,7 +141,12 @@ def test_dump_data_sources_dumps_all_databases_separately_when_format_configured dry_run_label=object, ).and_return(process).once() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes + assert ( + module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) + == processes + ) def test_database_names_to_dump_runs_mariadb_with_list_options(): @@ -434,7 +448,9 @@ def test_dump_data_sources_errors_for_missing_all_databases(): flexmock(module).should_receive('database_names_to_dump').and_return(()) with pytest.raises(ValueError): - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) + assert module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run(): @@ -445,7 +461,12 @@ def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run ) flexmock(module).should_receive('database_names_to_dump').and_return(()) - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=True) == [] + assert ( + module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=True + ) + == [] + ) def test_restore_data_source_dump_runs_mariadb_to_restore(): @@ -473,6 +494,7 @@ def test_restore_data_source_dump_runs_mariadb_to_restore(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -501,6 +523,7 @@ def test_restore_data_source_dump_runs_mariadb_with_options(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -531,6 +554,7 @@ def test_restore_data_source_dump_runs_non_default_mariadb_with_options(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -568,6 +592,7 @@ def test_restore_data_source_dump_runs_mariadb_with_hostname_and_port(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -596,6 +621,7 @@ def test_restore_data_source_dump_runs_mariadb_with_username_and_password(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -645,6 +671,7 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_ 'username': 'cliusername', 'password': 'clipassword', }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -696,6 +723,7 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_ 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -717,4 +745,5 @@ def test_restore_data_source_dump_with_dry_run_skips_restore(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) diff --git a/tests/unit/hooks/test_mongodb.py b/tests/unit/hooks/test_mongodb.py index 86ac7b7e..b08dfaa4 100644 --- a/tests/unit/hooks/test_mongodb.py +++ b/tests/unit/hooks/test_mongodb.py @@ -41,7 +41,12 @@ def test_dump_data_sources_runs_mongodump_for_each_database(): run_to_completion=False, ).and_return(process).once() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes + assert ( + module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) + == processes + ) def test_dump_data_sources_with_dry_run_skips_mongodump(): @@ -53,7 +58,12 @@ def test_dump_data_sources_with_dry_run_skips_mongodump(): flexmock(module.dump).should_receive('create_named_pipe_for_dump').never() flexmock(module).should_receive('execute_command').never() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=True) == [] + assert ( + module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=True + ) + == [] + ) def test_dump_data_sources_runs_mongodump_with_hostname_and_port(): @@ -82,7 +92,9 @@ def test_dump_data_sources_runs_mongodump_with_hostname_and_port(): run_to_completion=False, ).and_return(process).once() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process] + assert module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) == [process] def test_dump_data_sources_runs_mongodump_with_username_and_password(): @@ -120,7 +132,9 @@ def test_dump_data_sources_runs_mongodump_with_username_and_password(): run_to_completion=False, ).and_return(process).once() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process] + assert module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) == [process] def test_dump_data_sources_runs_mongodump_with_directory_format(): @@ -137,7 +151,12 @@ def test_dump_data_sources_runs_mongodump_with_directory_format(): shell=True, ).and_return(flexmock()).once() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [] + assert ( + module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) + == [] + ) def test_dump_data_sources_runs_mongodump_with_options(): @@ -163,7 +182,9 @@ def test_dump_data_sources_runs_mongodump_with_options(): run_to_completion=False, ).and_return(process).once() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process] + assert module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) == [process] def test_dump_data_sources_runs_mongodumpall_for_all_databases(): @@ -181,7 +202,9 @@ def test_dump_data_sources_runs_mongodumpall_for_all_databases(): run_to_completion=False, ).and_return(process).once() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process] + assert module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) == [process] def test_build_dump_command_with_username_injection_attack_gets_escaped(): @@ -218,6 +241,7 @@ def test_restore_data_source_dump_runs_mongorestore(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -257,6 +281,7 @@ def test_restore_data_source_dump_runs_mongorestore_with_hostname_and_port(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -304,6 +329,7 @@ def test_restore_data_source_dump_runs_mongorestore_with_username_and_password() 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -359,6 +385,7 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_ 'username': 'cliusername', 'password': 'clipassword', }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -414,6 +441,7 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_ 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -443,6 +471,7 @@ def test_restore_data_source_dump_runs_mongorestore_with_options(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -480,6 +509,7 @@ def test_restore_databases_dump_runs_mongorestore_with_schemas(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -509,6 +539,7 @@ def test_restore_data_source_dump_runs_psql_for_all_database_dump(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -532,6 +563,7 @@ def test_restore_data_source_dump_with_dry_run_skips_restore(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -560,4 +592,5 @@ def test_restore_data_source_dump_without_extract_process_restores_from_disk(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) diff --git a/tests/unit/hooks/test_mysql.py b/tests/unit/hooks/test_mysql.py index fa6145ac..496e6663 100644 --- a/tests/unit/hooks/test_mysql.py +++ b/tests/unit/hooks/test_mysql.py @@ -73,7 +73,12 @@ def test_dump_data_sources_dumps_each_database(): dry_run_label=object, ).and_return(process).once() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes + assert ( + module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) + == processes + ) def test_dump_data_sources_dumps_with_password(): @@ -94,7 +99,9 @@ def test_dump_data_sources_dumps_with_password(): dry_run_label=object, ).and_return(process).once() - assert module.dump_data_sources([database], {}, 'test.yaml', dry_run=False) == [process] + assert module.dump_data_sources( + [database], {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) == [process] def test_dump_data_sources_dumps_all_databases_at_once(): @@ -112,7 +119,9 @@ def test_dump_data_sources_dumps_all_databases_at_once(): dry_run_label=object, ).and_return(process).once() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process] + assert module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) == [process] def test_dump_data_sources_dumps_all_databases_separately_when_format_configured(): @@ -132,7 +141,12 @@ def test_dump_data_sources_dumps_all_databases_separately_when_format_configured dry_run_label=object, ).and_return(process).once() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes + assert ( + module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) + == processes + ) def test_database_names_to_dump_runs_mysql_with_list_options(): @@ -432,7 +446,9 @@ def test_dump_data_sources_errors_for_missing_all_databases(): flexmock(module).should_receive('database_names_to_dump').and_return(()) with pytest.raises(ValueError): - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) + assert module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run(): @@ -443,7 +459,12 @@ def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run ) flexmock(module).should_receive('database_names_to_dump').and_return(()) - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=True) == [] + assert ( + module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=True + ) + == [] + ) def test_restore_data_source_dump_runs_mysql_to_restore(): @@ -471,6 +492,7 @@ def test_restore_data_source_dump_runs_mysql_to_restore(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -499,6 +521,7 @@ def test_restore_data_source_dump_runs_mysql_with_options(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -527,6 +550,7 @@ def test_restore_data_source_dump_runs_non_default_mysql_with_options(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -564,6 +588,7 @@ def test_restore_data_source_dump_runs_mysql_with_hostname_and_port(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -592,6 +617,7 @@ def test_restore_data_source_dump_runs_mysql_with_username_and_password(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -641,6 +667,7 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_ 'username': 'cliusername', 'password': 'clipassword', }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -692,6 +719,7 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_ 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -713,4 +741,5 @@ def test_restore_data_source_dump_with_dry_run_skips_restore(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) diff --git a/tests/unit/hooks/test_postgresql.py b/tests/unit/hooks/test_postgresql.py index 87f5dc0f..229a94e4 100644 --- a/tests/unit/hooks/test_postgresql.py +++ b/tests/unit/hooks/test_postgresql.py @@ -251,7 +251,12 @@ def test_dump_data_sources_runs_pg_dump_for_each_database(): run_to_completion=False, ).and_return(process).once() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes + assert ( + module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) + == processes + ) def test_dump_data_sources_raises_when_no_database_names_to_dump(): @@ -261,7 +266,9 @@ def test_dump_data_sources_raises_when_no_database_names_to_dump(): flexmock(module).should_receive('database_names_to_dump').and_return(()) with pytest.raises(ValueError): - module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) + module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) def test_dump_data_sources_does_not_raise_when_no_database_names_to_dump(): @@ -270,7 +277,9 @@ def test_dump_data_sources_does_not_raise_when_no_database_names_to_dump(): flexmock(module).should_receive('make_dump_path').and_return('') flexmock(module).should_receive('database_names_to_dump').and_return(()) - module.dump_data_sources(databases, {}, 'test.yaml', dry_run=True) == [] + module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=True + ) == [] def test_dump_data_sources_with_duplicate_dump_skips_pg_dump(): @@ -287,7 +296,12 @@ def test_dump_data_sources_with_duplicate_dump_skips_pg_dump(): flexmock(module.dump).should_receive('create_named_pipe_for_dump').never() flexmock(module).should_receive('execute_command').never() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [] + assert ( + module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) + == [] + ) def test_dump_data_sources_with_dry_run_skips_pg_dump(): @@ -304,7 +318,12 @@ def test_dump_data_sources_with_dry_run_skips_pg_dump(): flexmock(module.dump).should_receive('create_named_pipe_for_dump').never() flexmock(module).should_receive('execute_command').never() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=True) == [] + assert ( + module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=True + ) + == [] + ) def test_dump_data_sources_runs_pg_dump_with_hostname_and_port(): @@ -340,7 +359,9 @@ def test_dump_data_sources_runs_pg_dump_with_hostname_and_port(): run_to_completion=False, ).and_return(process).once() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process] + assert module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) == [process] def test_dump_data_sources_runs_pg_dump_with_username_and_password(): @@ -376,7 +397,9 @@ def test_dump_data_sources_runs_pg_dump_with_username_and_password(): run_to_completion=False, ).and_return(process).once() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process] + assert module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) == [process] def test_dump_data_sources_with_username_injection_attack_gets_escaped(): @@ -412,7 +435,9 @@ def test_dump_data_sources_with_username_injection_attack_gets_escaped(): run_to_completion=False, ).and_return(process).once() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process] + assert module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) == [process] def test_dump_data_sources_runs_pg_dump_with_directory_format(): @@ -443,7 +468,12 @@ def test_dump_data_sources_runs_pg_dump_with_directory_format(): extra_environment={'PGSSLMODE': 'disable'}, ).and_return(flexmock()).once() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [] + assert ( + module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) + == [] + ) def test_dump_data_sources_runs_pg_dump_with_options(): @@ -476,7 +506,9 @@ def test_dump_data_sources_runs_pg_dump_with_options(): run_to_completion=False, ).and_return(process).once() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process] + assert module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) == [process] def test_dump_data_sources_runs_pg_dumpall_for_all_databases(): @@ -498,7 +530,9 @@ def test_dump_data_sources_runs_pg_dumpall_for_all_databases(): run_to_completion=False, ).and_return(process).once() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process] + assert module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) == [process] def test_dump_data_sources_runs_non_default_pg_dump(): @@ -532,7 +566,9 @@ def test_dump_data_sources_runs_non_default_pg_dump(): run_to_completion=False, ).and_return(process).once() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process] + assert module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) == [process] def test_restore_data_source_dump_runs_pg_restore(): @@ -584,6 +620,7 @@ def test_restore_data_source_dump_runs_pg_restore(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -646,6 +683,7 @@ def test_restore_data_source_dump_runs_pg_restore_with_hostname_and_port(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -706,6 +744,7 @@ def test_restore_data_source_dump_runs_pg_restore_with_username_and_password(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -785,6 +824,7 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_ 'username': 'cliusername', 'password': 'clipassword', }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -864,6 +904,7 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_ 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -925,6 +966,7 @@ def test_restore_data_source_dump_runs_pg_restore_with_options(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -964,6 +1006,7 @@ def test_restore_data_source_dump_runs_psql_for_all_database_dump(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -1008,6 +1051,7 @@ def test_restore_data_source_dump_runs_psql_for_plain_database_dump(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -1077,6 +1121,7 @@ def test_restore_data_source_dump_runs_non_default_pg_restore_and_psql(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -1101,6 +1146,7 @@ def test_restore_data_source_dump_with_dry_run_skips_restore(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -1153,6 +1199,7 @@ def test_restore_data_source_dump_without_extract_process_restores_from_disk(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -1209,4 +1256,5 @@ def test_restore_data_source_dump_with_schemas_restores_schemas(): 'username': None, 'password': None, }, + borgmatic_runtime_directory='/run/borgmatic', ) diff --git a/tests/unit/hooks/test_sqlite.py b/tests/unit/hooks/test_sqlite.py index ea560e30..460a08d9 100644 --- a/tests/unit/hooks/test_sqlite.py +++ b/tests/unit/hooks/test_sqlite.py @@ -18,15 +18,20 @@ def test_use_streaming_false_for_no_databases(): def test_dump_data_sources_logs_and_skips_if_dump_already_exists(): databases = [{'path': '/path/to/database', 'name': 'database'}] - flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump') + flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic') flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return( - '/path/to/dump/database' + '/run/borgmatic/database' ) flexmock(module.os.path).should_receive('exists').and_return(True) flexmock(module.dump).should_receive('create_named_pipe_for_dump').never() flexmock(module).should_receive('execute_command').never() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [] + assert ( + module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) + == [] + ) def test_dump_data_sources_dumps_each_database(): @@ -36,9 +41,9 @@ def test_dump_data_sources_dumps_each_database(): ] processes = [flexmock(), flexmock()] - flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump') + flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic') flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return( - '/path/to/dump/database' + '/run/borgmatic/database' ) flexmock(module.os.path).should_receive('exists').and_return(False) flexmock(module.dump).should_receive('create_named_pipe_for_dump') @@ -46,7 +51,12 @@ def test_dump_data_sources_dumps_each_database(): processes[1] ) - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes + assert ( + module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) + == processes + ) def test_dump_data_sources_with_path_injection_attack_gets_escaped(): @@ -55,9 +65,9 @@ def test_dump_data_sources_with_path_injection_attack_gets_escaped(): ] processes = [flexmock()] - flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump') + flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic') flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return( - '/path/to/dump/database' + '/run/borgmatic/database' ) flexmock(module.os.path).should_receive('exists').and_return(False) flexmock(module.dump).should_receive('create_named_pipe_for_dump') @@ -67,13 +77,18 @@ def test_dump_data_sources_with_path_injection_attack_gets_escaped(): "'/path/to/database1; naughty-command'", '.dump', '>', - '/path/to/dump/database', + '/run/borgmatic/database', ), shell=True, run_to_completion=False, ).and_return(processes[0]) - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes + assert ( + module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) + == processes + ) def test_dump_data_sources_with_non_existent_path_warns_and_dumps_database(): @@ -82,16 +97,21 @@ def test_dump_data_sources_with_non_existent_path_warns_and_dumps_database(): ] processes = [flexmock()] - flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump') + flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic') flexmock(module.logger).should_receive('warning').once() flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return( - '/path/to/dump/database' + '/run/borgmatic' ) flexmock(module.os.path).should_receive('exists').and_return(False) flexmock(module.dump).should_receive('create_named_pipe_for_dump') flexmock(module).should_receive('execute_command').and_return(processes[0]) - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes + assert ( + module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) + == processes + ) def test_dump_data_sources_with_name_all_warns_and_dumps_all_databases(): @@ -100,32 +120,42 @@ def test_dump_data_sources_with_name_all_warns_and_dumps_all_databases(): ] processes = [flexmock()] - flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump') + flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic') flexmock(module.logger).should_receive( 'warning' ).twice() # once for the name=all, once for the non-existent path flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return( - '/path/to/dump/database' + '/run/borgmatic/database' ) flexmock(module.os.path).should_receive('exists').and_return(False) flexmock(module.dump).should_receive('create_named_pipe_for_dump') flexmock(module).should_receive('execute_command').and_return(processes[0]) - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes + assert ( + module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False + ) + == processes + ) def test_dump_data_sources_does_not_dump_if_dry_run(): databases = [{'path': '/path/to/database', 'name': 'database'}] - flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump') + flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic') flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return( - '/path/to/dump/database' + '/run/borgmatic' ) flexmock(module.os.path).should_receive('exists').and_return(False) flexmock(module.dump).should_receive('create_named_pipe_for_dump').never() flexmock(module).should_receive('execute_command').never() - assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=True) == [] + assert ( + module.dump_data_sources( + databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=True + ) + == [] + ) def test_restore_data_source_dump_restores_database(): @@ -152,6 +182,7 @@ def test_restore_data_source_dump_restores_database(): dry_run=False, extract_process=extract_process, connection_params={'restore_path': None}, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -181,6 +212,7 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_ dry_run=False, extract_process=extract_process, connection_params={'restore_path': 'cli/path/to/database'}, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -210,6 +242,7 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_ dry_run=False, extract_process=extract_process, connection_params={'restore_path': None}, + borgmatic_runtime_directory='/run/borgmatic', ) @@ -228,4 +261,5 @@ def test_restore_data_source_dump_does_not_restore_database_if_dry_run(): dry_run=True, extract_process=extract_process, connection_params={'restore_path': None}, + borgmatic_runtime_directory='/run/borgmatic', )