From 5dc8450c8ea7a688f4fe9651d61667a3b25505a8 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Sun, 24 Nov 2024 16:15:12 -0800 Subject: [PATCH] Adding missing bootstrap files. --- borgmatic/hooks/bootstrap.py | 126 ++++++++++++++++++++++++++ tests/unit/hooks/test_bootstrap.py | 139 +++++++++++++++++++++++++++++ 2 files changed, 265 insertions(+) create mode 100644 borgmatic/hooks/bootstrap.py create mode 100644 tests/unit/hooks/test_bootstrap.py diff --git a/borgmatic/hooks/bootstrap.py b/borgmatic/hooks/bootstrap.py new file mode 100644 index 00000000..7ef209ab --- /dev/null +++ b/borgmatic/hooks/bootstrap.py @@ -0,0 +1,126 @@ +import glob +import importlib +import json +import logging +import os + +import borgmatic.config.paths + +logger = logging.getLogger(__name__) + + +def use_streaming(hook_config, config, log_prefix): # pragma: no cover + ''' + Return whether dump streaming is used for this hook. (Spoiler: It isn't.) + ''' + return False + + +def dump_data_sources( + hook_config, + config, + log_prefix, + config_paths, + borgmatic_runtime_directory, + source_directories, + dry_run, +): + ''' + Given a bootstrap configuration dict, a configuration dict, a log prefix, the borgmatic + configuration file paths, the borgmatic runtime directory, the configured source directories, + and whether this is a dry run, create a borgmatic manifest file to store the paths of the + configuration files used to create the archive. But skip this if the bootstrap + store_config_files option is False or if this is a dry run. + + Return an empty sequence, since there are no ongoing dump processes from this hook. + ''' + if hook_config.get('store_config_files') is False: + return [] + + borgmatic_manifest_path = os.path.join( + borgmatic_runtime_directory, 'bootstrap', 'manifest.json' + ) + + if dry_run: + return [] + + os.makedirs(os.path.dirname(borgmatic_manifest_path), exist_ok=True) + + with open(borgmatic_manifest_path, 'w') as manifest_file: + json.dump( + { + 'borgmatic_version': importlib.metadata.version('borgmatic'), + 'config_paths': config_paths, + }, + manifest_file, + ) + + source_directories.extend(config_paths) + source_directories.append(os.path.join(borgmatic_runtime_directory, 'bootstrap')) + + return [] + + +def remove_data_source_dumps(hook_config, config, log_prefix, borgmatic_runtime_directory, dry_run): + ''' + Given a bootstrap configuration dict, a configuration dict, a log prefix, the borgmatic runtime + directory, and whether this is a dry run, then remove the manifest file created above. If this + is a dry run, then don't actually remove anything. + ''' + dry_run_label = ' (dry run; not actually removing anything)' if dry_run else '' + + manifest_glob = os.path.join( + borgmatic.config.paths.replace_temporary_subdirectory_with_glob( + os.path.normpath(borgmatic_runtime_directory), + ), + 'bootstrap', + ) + logger.debug( + f'{log_prefix}: Looking for bootstrap manifest files to remove in {manifest_glob}{dry_run_label}' + ) + + for manifest_directory in glob.glob(manifest_glob): + manifest_file_path = os.path.join(manifest_directory, 'manifest.json') + logger.debug( + f'{log_prefix}: Removing bootstrap manifest at {manifest_file_path}{dry_run_label}' + ) + + if dry_run: + continue + + try: + os.remove(manifest_file_path) + except FileNotFoundError: + pass + + try: + os.rmdir(manifest_directory) + except FileNotFoundError: + pass + + +def make_data_source_dump_patterns( + hook_config, config, log_prefix, borgmatic_runtime_directory, name=None +): # pragma: no cover + ''' + Restores are implemented via the separate, purpose-specific "bootstrap" action rather than the + generic "restore". + ''' + return () + + +def restore_data_source_dump( + hook_config, + config, + log_prefix, + data_source, + dry_run, + extract_process, + connection_params, + borgmatic_runtime_directory, +): # pragma: no cover + ''' + Restores are implemented via the separate, purpose-specific "bootstrap" action rather than the + generic "restore". + ''' + raise NotImplementedError() diff --git a/tests/unit/hooks/test_bootstrap.py b/tests/unit/hooks/test_bootstrap.py new file mode 100644 index 00000000..3db97381 --- /dev/null +++ b/tests/unit/hooks/test_bootstrap.py @@ -0,0 +1,139 @@ +import sys + +from flexmock import flexmock + +from borgmatic.hooks import bootstrap as module + + +def test_dump_data_sources_creates_manifest_file(): + flexmock(module.os).should_receive('makedirs') + + flexmock(module.importlib.metadata).should_receive('version').and_return('1.0.0') + manifest_file = flexmock( + __enter__=lambda *args: flexmock(write=lambda *args: None, close=lambda *args: None), + __exit__=lambda *args: None, + ) + flexmock(sys.modules['builtins']).should_receive('open').with_args( + '/run/borgmatic/bootstrap/manifest.json', 'w' + ).and_return(manifest_file) + flexmock(module.json).should_receive('dump').with_args( + {'borgmatic_version': '1.0.0', 'config_paths': ('test.yaml',)}, + manifest_file, + ).once() + + module.dump_data_sources( + hook_config={}, + config={}, + log_prefix='test', + config_paths=('test.yaml',), + borgmatic_runtime_directory='/run/borgmatic', + source_directories=[], + dry_run=False, + ) + + +def test_dump_data_sources_with_store_config_files_false_does_not_create_manifest_file(): + flexmock(module.os).should_receive('makedirs').never() + flexmock(module.json).should_receive('dump').never() + hook_config = {'store_config_files': False} + + module.dump_data_sources( + hook_config=hook_config, + config={'bootstrap': hook_config}, + log_prefix='test', + config_paths=('test.yaml',), + borgmatic_runtime_directory='/run/borgmatic', + source_directories=[], + dry_run=True, + ) + + +def test_dump_data_sources_with_dry_run_does_not_create_manifest_file(): + flexmock(module.os).should_receive('makedirs').never() + flexmock(module.json).should_receive('dump').never() + + module.dump_data_sources( + hook_config={}, + config={}, + log_prefix='test', + config_paths=('test.yaml',), + borgmatic_runtime_directory='/run/borgmatic', + source_directories=[], + dry_run=True, + ) + + +def test_remove_data_source_dumps_deletes_manifest_and_parent_directory(): + flexmock(module.borgmatic.config.paths).should_receive( + 'replace_temporary_subdirectory_with_glob' + ).and_return('/run/borgmatic') + flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path]) + flexmock(module.os).should_receive('remove').with_args( + '/run/borgmatic/bootstrap/manifest.json' + ).once() + flexmock(module.os).should_receive('rmdir').with_args('/run/borgmatic/bootstrap').once() + + module.remove_data_source_dumps( + hook_config={}, + config={}, + log_prefix='test', + borgmatic_runtime_directory='/run/borgmatic', + dry_run=False, + ) + + +def test_remove_data_source_dumps_with_dry_run_bails(): + flexmock(module.borgmatic.config.paths).should_receive( + 'replace_temporary_subdirectory_with_glob' + ).and_return('/run/borgmatic') + flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path]) + flexmock(module.os).should_receive('remove').never() + flexmock(module.os).should_receive('rmdir').never() + + module.remove_data_source_dumps( + hook_config={}, + config={}, + log_prefix='test', + borgmatic_runtime_directory='/run/borgmatic', + dry_run=True, + ) + + +def test_remove_data_source_dumps_swallows_manifest_file_not_found_error(): + flexmock(module.borgmatic.config.paths).should_receive( + 'replace_temporary_subdirectory_with_glob' + ).and_return('/run/borgmatic') + flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path]) + flexmock(module.os).should_receive('remove').with_args( + '/run/borgmatic/bootstrap/manifest.json' + ).and_raise(FileNotFoundError).once() + flexmock(module.os).should_receive('rmdir').with_args('/run/borgmatic/bootstrap').once() + + module.remove_data_source_dumps( + hook_config={}, + config={}, + log_prefix='test', + borgmatic_runtime_directory='/run/borgmatic', + dry_run=False, + ) + + +def test_remove_data_source_dumps_swallows_manifest_parent_directory_not_found_error(): + flexmock(module.borgmatic.config.paths).should_receive( + 'replace_temporary_subdirectory_with_glob' + ).and_return('/run/borgmatic') + flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path]) + flexmock(module.os).should_receive('remove').with_args( + '/run/borgmatic/bootstrap/manifest.json' + ).once() + flexmock(module.os).should_receive('rmdir').with_args('/run/borgmatic/bootstrap').and_raise( + FileNotFoundError + ).once() + + module.remove_data_source_dumps( + hook_config={}, + config={}, + log_prefix='test', + borgmatic_runtime_directory='/run/borgmatic', + dry_run=False, + )