浏览代码

Adding missing bootstrap files.

Dan Helfman 6 月之前
父节点
当前提交
5dc8450c8e
共有 2 个文件被更改,包括 265 次插入0 次删除
  1. 126 0
      borgmatic/hooks/bootstrap.py
  2. 139 0
      tests/unit/hooks/test_bootstrap.py

+ 126 - 0
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()

+ 139 - 0
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,
+    )