diff --git a/borgmatic/config/paths.py b/borgmatic/config/paths.py index ecc64b3b..e5786237 100644 --- a/borgmatic/config/paths.py +++ b/borgmatic/config/paths.py @@ -33,7 +33,9 @@ def get_borgmatic_source_directory(config): TEMPORARY_DIRECTORY_PREFIX = 'borgmatic-' -def replace_temporary_subdirectory_with_glob(path, temporary_directory_prefix=TEMPORARY_DIRECTORY_PREFIX): +def replace_temporary_subdirectory_with_glob( + path, temporary_directory_prefix=TEMPORARY_DIRECTORY_PREFIX +): ''' Given an absolute temporary directory path and an optional temporary directory prefix, look for a subdirectory within it starting with the temporary directory prefix (or a default) and replace diff --git a/borgmatic/hooks/data_source/btrfs.py b/borgmatic/hooks/data_source/btrfs.py index de9fe265..8c78620c 100644 --- a/borgmatic/hooks/data_source/btrfs.py +++ b/borgmatic/hooks/data_source/btrfs.py @@ -30,13 +30,7 @@ def get_filesystem_mount_points(findmnt_command): ) ) - try: - return tuple( - line.rstrip().split(' ')[0] - for line in findmnt_output.splitlines() - ) - except ValueError: - raise ValueError('Invalid {findmnt_command} output') + return tuple(line.rstrip().split(' ')[0] for line in findmnt_output.splitlines()) def get_subvolumes_for_filesystem(btrfs_command, filesystem_mount_point): @@ -53,15 +47,12 @@ def get_subvolumes_for_filesystem(btrfs_command, filesystem_mount_point): ) ) - try: - return tuple( - subvolume_path - for line in btrfs_output.splitlines() - for subvolume_subpath in (line.rstrip().split(' ')[-1],) - for subvolume_path in (os.path.join(filesystem_mount_point, subvolume_subpath),) - ) - except ValueError: - raise ValueError('Invalid {btrfs_command} subvolume list output') + return tuple( + subvolume_path + for line in btrfs_output.splitlines() + for subvolume_subpath in (line.rstrip().split(' ')[-1],) + for subvolume_path in (os.path.join(filesystem_mount_point, subvolume_subpath),) + ) def get_subvolumes(btrfs_command, findmnt_command, source_directories=None): diff --git a/tests/unit/hooks/data_source/test_btrfs.py b/tests/unit/hooks/data_source/test_btrfs.py new file mode 100644 index 00000000..d9bc2b42 --- /dev/null +++ b/tests/unit/hooks/data_source/test_btrfs.py @@ -0,0 +1,602 @@ +import pytest +from flexmock import flexmock + +from borgmatic.hooks.data_source import btrfs as module + + +def test_get_filesystem_mount_points_parses_findmnt_output(): + flexmock(module.borgmatic.execute).should_receive( + 'execute_command_and_capture_output' + ).and_return( + '/mnt0 /dev/loop0 btrfs rw,relatime,ssd,space_cache=v2,subvolid=5,subvol=/\n' + '/mnt1 /dev/loop1 btrfs rw,relatime,ssd,space_cache=v2,subvolid=5,subvol=/\n' + ) + + assert module.get_filesystem_mount_points('findmnt') == ('/mnt0', '/mnt1') + + +def test_get_subvolumes_for_filesystem_parses_subvolume_list_output(): + flexmock(module.borgmatic.execute).should_receive( + 'execute_command_and_capture_output' + ).and_return( + 'ID 270 gen 107 top level 5 path subvol1\n' 'ID 272 gen 74 top level 5 path subvol2\n' + ) + + assert module.get_subvolumes_for_filesystem('btrfs', '/mnt') == ('/mnt/subvol1', '/mnt/subvol2') + + +def test_get_subvolumes_collects_subvolumes_matching_source_directories_from_all_filesystems(): + flexmock(module).should_receive('get_filesystem_mount_points').and_return(('/mnt1', '/mnt2')) + flexmock(module).should_receive('get_subvolumes_for_filesystem').with_args( + 'btrfs', '/mnt1' + ).and_return(('/one', '/two')) + flexmock(module).should_receive('get_subvolumes_for_filesystem').with_args( + 'btrfs', '/mnt2' + ).and_return(('/three', '/four')) + + assert module.get_subvolumes( + 'btrfs', 'findmnt', source_directories=['/one', '/four', '/five', '/six', '/mnt2', '/mnt3'] + ) == ('/one', '/mnt2', '/four') + + +def test_get_subvolumes_without_source_directories_collects_all_subvolumes_from_all_filesystems(): + flexmock(module).should_receive('get_filesystem_mount_points').and_return(('/mnt1', '/mnt2')) + flexmock(module).should_receive('get_subvolumes_for_filesystem').with_args( + 'btrfs', '/mnt1' + ).and_return(('/one', '/two')) + flexmock(module).should_receive('get_subvolumes_for_filesystem').with_args( + 'btrfs', '/mnt2' + ).and_return(('/three', '/four')) + + assert module.get_subvolumes('btrfs', 'findmnt') == ( + '/mnt1', + '/one', + '/two', + '/mnt2', + '/three', + '/four', + ) + + +def test_dump_data_sources_snapshots_each_subvolume_and_updates_source_directories(): + source_directories = ['/foo', '/mnt/subvol1'] + config = {'btrfs': {}} + flexmock(module).should_receive('get_subvolumes').and_return(('/mnt/subvol1', '/mnt/subvol2')) + flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return( + '/mnt/subvol1/.borgmatic-1234/mnt/subvol1' + ) + flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return( + '/mnt/subvol2/.borgmatic-1234/mnt/subvol2' + ) + flexmock(module).should_receive('snapshot_subvolume').with_args( + 'btrfs', '/mnt/subvol1', '/mnt/subvol1/.borgmatic-1234/mnt/subvol1' + ).once() + flexmock(module).should_receive('snapshot_subvolume').with_args( + 'btrfs', '/mnt/subvol2', '/mnt/subvol2/.borgmatic-1234/mnt/subvol2' + ).once() + flexmock(module).should_receive('make_snapshot_exclude_path').with_args( + '/mnt/subvol1' + ).and_return('/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234') + flexmock(module).should_receive('make_snapshot_exclude_path').with_args( + '/mnt/subvol2' + ).and_return('/mnt/subvol2/.borgmatic-1234/mnt/subvol2/.borgmatic-1234') + + assert ( + module.dump_data_sources( + hook_config=config['btrfs'], + config=config, + log_prefix='test', + config_paths=('test.yaml',), + borgmatic_runtime_directory='/run/borgmatic', + source_directories=source_directories, + dry_run=False, + ) + == [] + ) + + assert source_directories == [ + '/foo', + '/mnt/subvol1/.borgmatic-1234/mnt/subvol1', + '/mnt/subvol2/.borgmatic-1234/mnt/subvol2', + ] + assert config == { + 'btrfs': {}, + 'exclude_patterns': [ + '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234', + '/mnt/subvol2/.borgmatic-1234/mnt/subvol2/.borgmatic-1234', + ], + } + + +def test_dump_data_sources_uses_custom_btrfs_command_in_commands(): + source_directories = ['/foo', '/mnt/subvol1'] + config = {'btrfs': {'btrfs_command': '/usr/local/bin/btrfs'}} + flexmock(module).should_receive('get_subvolumes').and_return(('/mnt/subvol1',)) + flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return( + '/mnt/subvol1/.borgmatic-1234/mnt/subvol1' + ) + flexmock(module).should_receive('snapshot_subvolume').with_args( + '/usr/local/bin/btrfs', '/mnt/subvol1', '/mnt/subvol1/.borgmatic-1234/mnt/subvol1' + ).once() + flexmock(module).should_receive('make_snapshot_exclude_path').with_args( + '/mnt/subvol1' + ).and_return('/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234') + + assert ( + module.dump_data_sources( + hook_config=config['btrfs'], + config=config, + log_prefix='test', + config_paths=('test.yaml',), + borgmatic_runtime_directory='/run/borgmatic', + source_directories=source_directories, + dry_run=False, + ) + == [] + ) + + assert source_directories == [ + '/foo', + '/mnt/subvol1/.borgmatic-1234/mnt/subvol1', + ] + assert config == { + 'btrfs': { + 'btrfs_command': '/usr/local/bin/btrfs', + }, + 'exclude_patterns': [ + '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234', + ], + } + + +def test_dump_data_sources_uses_custom_findmnt_command_in_commands(): + source_directories = ['/foo', '/mnt/subvol1'] + config = {'btrfs': {'findmnt_command': '/usr/local/bin/findmnt'}} + flexmock(module).should_receive('get_subvolumes').with_args( + 'btrfs', '/usr/local/bin/findmnt', source_directories + ).and_return(('/mnt/subvol1',)).once() + flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return( + '/mnt/subvol1/.borgmatic-1234/mnt/subvol1' + ) + flexmock(module).should_receive('snapshot_subvolume').with_args( + 'btrfs', '/mnt/subvol1', '/mnt/subvol1/.borgmatic-1234/mnt/subvol1' + ).once() + flexmock(module).should_receive('make_snapshot_exclude_path').with_args( + '/mnt/subvol1' + ).and_return('/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234') + + assert ( + module.dump_data_sources( + hook_config=config['btrfs'], + config=config, + log_prefix='test', + config_paths=('test.yaml',), + borgmatic_runtime_directory='/run/borgmatic', + source_directories=source_directories, + dry_run=False, + ) + == [] + ) + + assert source_directories == [ + '/foo', + '/mnt/subvol1/.borgmatic-1234/mnt/subvol1', + ] + assert config == { + 'btrfs': { + 'findmnt_command': '/usr/local/bin/findmnt', + }, + 'exclude_patterns': [ + '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234', + ], + } + + +def test_dump_data_sources_with_dry_run_skips_snapshot_and_source_directories_update(): + source_directories = ['/foo', '/mnt/subvol1'] + config = {'btrfs': {}} + flexmock(module).should_receive('get_subvolumes').and_return(('/mnt/subvol1',)) + flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return( + '/mnt/subvol1/.borgmatic-1234/mnt/subvol1' + ) + flexmock(module).should_receive('snapshot_subvolume').never() + flexmock(module).should_receive('make_snapshot_exclude_path').never() + + assert ( + module.dump_data_sources( + hook_config=config['btrfs'], + config=config, + log_prefix='test', + config_paths=('test.yaml',), + borgmatic_runtime_directory='/run/borgmatic', + source_directories=source_directories, + dry_run=True, + ) + == [] + ) + + assert source_directories == ['/foo', '/mnt/subvol1'] + assert config == {'btrfs': {}} + + +def test_dump_data_sources_without_matching_subvolumes_skips_snapshot_and_source_directories_update(): + source_directories = ['/foo', '/mnt/subvol1'] + config = {'btrfs': {}} + flexmock(module).should_receive('get_subvolumes').and_return(()) + flexmock(module).should_receive('make_snapshot_path').never() + flexmock(module).should_receive('snapshot_subvolume').never() + flexmock(module).should_receive('make_snapshot_exclude_path').never() + + assert ( + module.dump_data_sources( + hook_config=config['btrfs'], + config=config, + log_prefix='test', + config_paths=('test.yaml',), + borgmatic_runtime_directory='/run/borgmatic', + source_directories=source_directories, + dry_run=False, + ) + == [] + ) + + assert source_directories == ['/foo', '/mnt/subvol1'] + assert config == {'btrfs': {}} + + +def test_remove_data_source_dumps_deletes_snapshots(): + config = {'btrfs': {}} + flexmock(module).should_receive('get_subvolumes').and_return(('/mnt/subvol1', '/mnt/subvol2')) + flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return( + '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1' + ) + flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return( + '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2' + ) + flexmock(module.borgmatic.config.paths).should_receive( + 'replace_temporary_subdirectory_with_glob' + ).with_args( + '/mnt/subvol1/.borgmatic-1234/mnt/subvol1', + temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX, + ).and_return( + '/mnt/subvol1/.borgmatic-*/mnt/subvol1' + ) + flexmock(module.borgmatic.config.paths).should_receive( + 'replace_temporary_subdirectory_with_glob' + ).with_args( + '/mnt/subvol2/.borgmatic-1234/mnt/subvol2', + temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX, + ).and_return( + '/mnt/subvol2/.borgmatic-*/mnt/subvol2' + ) + flexmock(module.glob).should_receive('glob').with_args( + '/mnt/subvol1/.borgmatic-*/mnt/subvol1' + ).and_return( + ('/mnt/subvol1/.borgmatic-1234/mnt/subvol1', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1') + ) + flexmock(module.glob).should_receive('glob').with_args( + '/mnt/subvol2/.borgmatic-*/mnt/subvol2' + ).and_return( + ('/mnt/subvol2/.borgmatic-1234/mnt/subvol2', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2') + ) + flexmock(module.os.path).should_receive('isdir').with_args( + '/mnt/subvol1/.borgmatic-1234/mnt/subvol1' + ).and_return(True) + flexmock(module.os.path).should_receive('isdir').with_args( + '/mnt/subvol1/.borgmatic-5678/mnt/subvol1' + ).and_return(True) + flexmock(module.os.path).should_receive('isdir').with_args( + '/mnt/subvol2/.borgmatic-1234/mnt/subvol2' + ).and_return(True) + flexmock(module.os.path).should_receive('isdir').with_args( + '/mnt/subvol2/.borgmatic-5678/mnt/subvol2' + ).and_return(False) + flexmock(module).should_receive('delete_snapshot').with_args( + 'btrfs', '/mnt/subvol1/.borgmatic-1234/mnt/subvol1' + ).once() + flexmock(module).should_receive('delete_snapshot').with_args( + 'btrfs', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1' + ).once() + flexmock(module).should_receive('delete_snapshot').with_args( + 'btrfs', '/mnt/subvol2/.borgmatic-1234/mnt/subvol2' + ).once() + flexmock(module).should_receive('delete_snapshot').with_args( + 'btrfs', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2' + ).never() + flexmock(module.shutil).should_receive('rmtree').with_args( + '/mnt/subvol1/.borgmatic-1234' + ).once() + flexmock(module.shutil).should_receive('rmtree').with_args( + '/mnt/subvol1/.borgmatic-5678' + ).once() + flexmock(module.shutil).should_receive('rmtree').with_args( + '/mnt/subvol2/.borgmatic-1234' + ).once() + flexmock(module.shutil).should_receive('rmtree').with_args( + '/mnt/subvol2/.borgmatic-5678' + ).never() + + module.remove_data_source_dumps( + hook_config=config['btrfs'], + config=config, + log_prefix='test', + borgmatic_runtime_directory='/run/borgmatic', + dry_run=False, + ) + + +def test_remove_data_source_dumps_with_get_subvolumes_file_not_found_error_bails(): + config = {'btrfs': {}} + flexmock(module).should_receive('get_subvolumes').and_raise(FileNotFoundError) + flexmock(module).should_receive('make_snapshot_path').never() + flexmock(module.borgmatic.config.paths).should_receive( + 'replace_temporary_subdirectory_with_glob' + ).never() + flexmock(module).should_receive('delete_snapshot').never() + flexmock(module.shutil).should_receive('rmtree').never() + + module.remove_data_source_dumps( + hook_config=config['btrfs'], + config=config, + log_prefix='test', + borgmatic_runtime_directory='/run/borgmatic', + dry_run=False, + ) + + +def test_remove_data_source_dumps_with_get_subvolumes_called_process_error_bails(): + config = {'btrfs': {}} + flexmock(module).should_receive('get_subvolumes').and_raise( + module.subprocess.CalledProcessError(1, 'command', 'error') + ) + flexmock(module).should_receive('make_snapshot_path').never() + flexmock(module.borgmatic.config.paths).should_receive( + 'replace_temporary_subdirectory_with_glob' + ).never() + flexmock(module).should_receive('delete_snapshot').never() + flexmock(module.shutil).should_receive('rmtree').never() + + module.remove_data_source_dumps( + hook_config=config['btrfs'], + config=config, + log_prefix='test', + borgmatic_runtime_directory='/run/borgmatic', + dry_run=False, + ) + + +def test_remove_data_source_dumps_with_dry_run_skips_deletes(): + config = {'btrfs': {}} + flexmock(module).should_receive('get_subvolumes').and_return(('/mnt/subvol1', '/mnt/subvol2')) + flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return( + '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1' + ) + flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return( + '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2' + ) + flexmock(module.borgmatic.config.paths).should_receive( + 'replace_temporary_subdirectory_with_glob' + ).with_args( + '/mnt/subvol1/.borgmatic-1234/mnt/subvol1', + temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX, + ).and_return( + '/mnt/subvol1/.borgmatic-*/mnt/subvol1' + ) + flexmock(module.borgmatic.config.paths).should_receive( + 'replace_temporary_subdirectory_with_glob' + ).with_args( + '/mnt/subvol2/.borgmatic-1234/mnt/subvol2', + temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX, + ).and_return( + '/mnt/subvol2/.borgmatic-*/mnt/subvol2' + ) + flexmock(module.glob).should_receive('glob').with_args( + '/mnt/subvol1/.borgmatic-*/mnt/subvol1' + ).and_return( + ('/mnt/subvol1/.borgmatic-1234/mnt/subvol1', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1') + ) + flexmock(module.glob).should_receive('glob').with_args( + '/mnt/subvol2/.borgmatic-*/mnt/subvol2' + ).and_return( + ('/mnt/subvol2/.borgmatic-1234/mnt/subvol2', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2') + ) + flexmock(module.os.path).should_receive('isdir').with_args( + '/mnt/subvol1/.borgmatic-1234/mnt/subvol1' + ).and_return(True) + flexmock(module.os.path).should_receive('isdir').with_args( + '/mnt/subvol1/.borgmatic-5678/mnt/subvol1' + ).and_return(True) + flexmock(module.os.path).should_receive('isdir').with_args( + '/mnt/subvol2/.borgmatic-1234/mnt/subvol2' + ).and_return(True) + flexmock(module.os.path).should_receive('isdir').with_args( + '/mnt/subvol2/.borgmatic-5678/mnt/subvol2' + ).and_return(False) + flexmock(module).should_receive('delete_snapshot').never() + flexmock(module.shutil).should_receive('rmtree').never() + + module.remove_data_source_dumps( + hook_config=config['btrfs'], + config=config, + log_prefix='test', + borgmatic_runtime_directory='/run/borgmatic', + dry_run=True, + ) + + +def test_remove_data_source_dumps_without_subvolumes_skips_deletes(): + config = {'btrfs': {}} + flexmock(module).should_receive('get_subvolumes').and_return(()) + flexmock(module).should_receive('make_snapshot_path').never() + flexmock(module.borgmatic.config.paths).should_receive( + 'replace_temporary_subdirectory_with_glob' + ).never() + flexmock(module).should_receive('delete_snapshot').never() + flexmock(module.shutil).should_receive('rmtree').never() + + module.remove_data_source_dumps( + hook_config=config['btrfs'], + config=config, + log_prefix='test', + borgmatic_runtime_directory='/run/borgmatic', + dry_run=False, + ) + + +def test_remove_data_source_without_snapshots_skips_deletes(): + config = {'btrfs': {}} + flexmock(module).should_receive('get_subvolumes').and_return(('/mnt/subvol1', '/mnt/subvol2')) + flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return( + '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1' + ) + flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return( + '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2' + ) + flexmock(module.borgmatic.config.paths).should_receive( + 'replace_temporary_subdirectory_with_glob' + ).with_args( + '/mnt/subvol1/.borgmatic-1234/mnt/subvol1', + temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX, + ).and_return( + '/mnt/subvol1/.borgmatic-*/mnt/subvol1' + ) + flexmock(module.borgmatic.config.paths).should_receive( + 'replace_temporary_subdirectory_with_glob' + ).with_args( + '/mnt/subvol2/.borgmatic-1234/mnt/subvol2', + temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX, + ).and_return( + '/mnt/subvol2/.borgmatic-*/mnt/subvol2' + ) + flexmock(module.glob).should_receive('glob').and_return(()) + flexmock(module.os.path).should_receive('isdir').never() + flexmock(module).should_receive('delete_snapshot').never() + flexmock(module.shutil).should_receive('rmtree').never() + + module.remove_data_source_dumps( + hook_config=config['btrfs'], + config=config, + log_prefix='test', + borgmatic_runtime_directory='/run/borgmatic', + dry_run=False, + ) + + +def test_remove_data_source_dumps_with_delete_snapshot_file_not_found_error_bails(): + config = {'btrfs': {}} + flexmock(module).should_receive('get_subvolumes').and_return(('/mnt/subvol1', '/mnt/subvol2')) + flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return( + '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1' + ) + flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return( + '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2' + ) + flexmock(module.borgmatic.config.paths).should_receive( + 'replace_temporary_subdirectory_with_glob' + ).with_args( + '/mnt/subvol1/.borgmatic-1234/mnt/subvol1', + temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX, + ).and_return( + '/mnt/subvol1/.borgmatic-*/mnt/subvol1' + ) + flexmock(module.borgmatic.config.paths).should_receive( + 'replace_temporary_subdirectory_with_glob' + ).with_args( + '/mnt/subvol2/.borgmatic-1234/mnt/subvol2', + temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX, + ).and_return( + '/mnt/subvol2/.borgmatic-*/mnt/subvol2' + ) + flexmock(module.glob).should_receive('glob').with_args( + '/mnt/subvol1/.borgmatic-*/mnt/subvol1' + ).and_return( + ('/mnt/subvol1/.borgmatic-1234/mnt/subvol1', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1') + ) + flexmock(module.glob).should_receive('glob').with_args( + '/mnt/subvol2/.borgmatic-*/mnt/subvol2' + ).and_return( + ('/mnt/subvol2/.borgmatic-1234/mnt/subvol2', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2') + ) + flexmock(module.os.path).should_receive('isdir').with_args( + '/mnt/subvol1/.borgmatic-1234/mnt/subvol1' + ).and_return(True) + flexmock(module.os.path).should_receive('isdir').with_args( + '/mnt/subvol1/.borgmatic-5678/mnt/subvol1' + ).and_return(True) + flexmock(module.os.path).should_receive('isdir').with_args( + '/mnt/subvol2/.borgmatic-1234/mnt/subvol2' + ).and_return(True) + flexmock(module.os.path).should_receive('isdir').with_args( + '/mnt/subvol2/.borgmatic-5678/mnt/subvol2' + ).and_return(False) + flexmock(module).should_receive('delete_snapshot').and_raise(FileNotFoundError) + flexmock(module.shutil).should_receive('rmtree').never() + + module.remove_data_source_dumps( + hook_config=config['btrfs'], + config=config, + log_prefix='test', + borgmatic_runtime_directory='/run/borgmatic', + dry_run=False, + ) + + +def test_remove_data_source_dumps_with_delete_snapshot_called_process_error_bails(): + config = {'btrfs': {}} + flexmock(module).should_receive('get_subvolumes').and_return(('/mnt/subvol1', '/mnt/subvol2')) + flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return( + '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1' + ) + flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return( + '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2' + ) + flexmock(module.borgmatic.config.paths).should_receive( + 'replace_temporary_subdirectory_with_glob' + ).with_args( + '/mnt/subvol1/.borgmatic-1234/mnt/subvol1', + temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX, + ).and_return( + '/mnt/subvol1/.borgmatic-*/mnt/subvol1' + ) + flexmock(module.borgmatic.config.paths).should_receive( + 'replace_temporary_subdirectory_with_glob' + ).with_args( + '/mnt/subvol2/.borgmatic-1234/mnt/subvol2', + temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX, + ).and_return( + '/mnt/subvol2/.borgmatic-*/mnt/subvol2' + ) + flexmock(module.glob).should_receive('glob').with_args( + '/mnt/subvol1/.borgmatic-*/mnt/subvol1' + ).and_return( + ('/mnt/subvol1/.borgmatic-1234/mnt/subvol1', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1') + ) + flexmock(module.glob).should_receive('glob').with_args( + '/mnt/subvol2/.borgmatic-*/mnt/subvol2' + ).and_return( + ('/mnt/subvol2/.borgmatic-1234/mnt/subvol2', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2') + ) + flexmock(module.os.path).should_receive('isdir').with_args( + '/mnt/subvol1/.borgmatic-1234/mnt/subvol1' + ).and_return(True) + flexmock(module.os.path).should_receive('isdir').with_args( + '/mnt/subvol1/.borgmatic-5678/mnt/subvol1' + ).and_return(True) + flexmock(module.os.path).should_receive('isdir').with_args( + '/mnt/subvol2/.borgmatic-1234/mnt/subvol2' + ).and_return(True) + flexmock(module.os.path).should_receive('isdir').with_args( + '/mnt/subvol2/.borgmatic-5678/mnt/subvol2' + ).and_return(False) + flexmock(module).should_receive('delete_snapshot').and_raise( + module.subprocess.CalledProcessError(1, 'command', 'error') + ) + flexmock(module.shutil).should_receive('rmtree').never() + + module.remove_data_source_dumps( + hook_config=config['btrfs'], + config=config, + log_prefix='test', + borgmatic_runtime_directory='/run/borgmatic', + dry_run=False, + ) diff --git a/tests/unit/hooks/data_source/test_zfs.py b/tests/unit/hooks/data_source/test_zfs.py index 7f634dad..cbb8cc9d 100644 --- a/tests/unit/hooks/data_source/test_zfs.py +++ b/tests/unit/hooks/data_source/test_zfs.py @@ -1,12 +1,13 @@ import pytest from flexmock import flexmock -import borgmatic.execute from borgmatic.hooks.data_source import zfs as module def test_get_datasets_to_backup_filters_datasets_by_source_directories(): - flexmock(borgmatic.execute).should_receive('execute_command_and_capture_output').and_return( + flexmock(module.borgmatic.execute).should_receive( + 'execute_command_and_capture_output' + ).and_return( 'dataset\t/dataset\t-\nother\t/other\t-', ) @@ -16,7 +17,9 @@ def test_get_datasets_to_backup_filters_datasets_by_source_directories(): def test_get_datasets_to_backup_filters_datasets_by_user_property(): - flexmock(borgmatic.execute).should_receive('execute_command_and_capture_output').and_return( + flexmock(module.borgmatic.execute).should_receive( + 'execute_command_and_capture_output' + ).and_return( 'dataset\t/dataset\tauto\nother\t/other\t-', ) @@ -26,7 +29,9 @@ def test_get_datasets_to_backup_filters_datasets_by_user_property(): def test_get_datasets_to_backup_with_invalid_list_output_raises(): - flexmock(borgmatic.execute).should_receive('execute_command_and_capture_output').and_return( + flexmock(module.borgmatic.execute).should_receive( + 'execute_command_and_capture_output' + ).and_return( 'dataset', ) @@ -35,7 +40,9 @@ def test_get_datasets_to_backup_with_invalid_list_output_raises(): def test_get_get_all_datasets_does_not_filter_datasets(): - flexmock(borgmatic.execute).should_receive('execute_command_and_capture_output').and_return( + flexmock(module.borgmatic.execute).should_receive( + 'execute_command_and_capture_output' + ).and_return( 'dataset\t/dataset\nother\t/other', ) @@ -46,7 +53,9 @@ def test_get_get_all_datasets_does_not_filter_datasets(): def test_get_all_datasets_with_invalid_list_output_raises(): - flexmock(borgmatic.execute).should_receive('execute_command_and_capture_output').and_return( + flexmock(module.borgmatic.execute).should_receive( + 'execute_command_and_capture_output' + ).and_return( 'dataset', ) @@ -155,7 +164,9 @@ def test_dump_data_sources_with_dry_run_skips_commands_and_does_not_touch_source def test_get_all_snapshots_parses_list_output(): - flexmock(borgmatic.execute).should_receive('execute_command_and_capture_output').and_return( + flexmock(module.borgmatic.execute).should_receive( + 'execute_command_and_capture_output' + ).and_return( 'dataset1@borgmatic-1234\ndataset2@borgmatic-4567', )