diff --git a/NEWS b/NEWS index 893e8047..04f07bcf 100644 --- a/NEWS +++ b/NEWS @@ -4,7 +4,7 @@ https://torsion.org/borgmatic/docs/how-to/provide-your-passwords/ * #996: Fix the "create" action to omit the repository label prefix from Borg's output when databases are enabled. - * #1001: For the ZFS, Btrfs, and LVM hooks, only make snapshots for patterns that come from + * #1001: For the ZFS, Btrfs, and LVM hooks, only make snapshots for root patterns that come from a borgmatic configuration option (e.g. "source_directories")—not from other hooks within borgmatic. * #1001: Fix a ZFS/LVM error due to colliding snapshot mount points for nested datasets or logical diff --git a/borgmatic/hooks/data_source/lvm.py b/borgmatic/hooks/data_source/lvm.py index 8743f3e1..1d2e4b42 100644 --- a/borgmatic/hooks/data_source/lvm.py +++ b/borgmatic/hooks/data_source/lvm.py @@ -237,7 +237,9 @@ def dump_data_sources( snapshot_mount_path = os.path.join( normalized_runtime_directory, 'lvm_snapshots', - hashlib.shake_256(dataset.mount_point.encode('utf-8')).hexdigest(MOUNT_POINT_HASH_LENGTH), + hashlib.shake_256(dataset.mount_point.encode('utf-8')).hexdigest( + MOUNT_POINT_HASH_LENGTH + ), logical_volume.mount_point.lstrip(os.path.sep), ) diff --git a/borgmatic/hooks/data_source/zfs.py b/borgmatic/hooks/data_source/zfs.py index 105eb215..61dfeb22 100644 --- a/borgmatic/hooks/data_source/zfs.py +++ b/borgmatic/hooks/data_source/zfs.py @@ -64,7 +64,9 @@ def get_datasets_to_backup(zfs_command, patterns): ( Dataset(dataset_name, mount_point, (user_property_value == 'auto'), ()) for line in list_output.splitlines() - for (dataset_name, mount_point, can_mount, user_property_value) in (line.rstrip().split('\t'),) + for (dataset_name, mount_point, can_mount, user_property_value) in ( + line.rstrip().split('\t'), + ) # Skip datasets that are marked "canmount=off", because mounting their snapshots will # result in completely empty mount points—thereby preventing us from backing them up. if can_mount == 'on' @@ -104,7 +106,8 @@ def get_datasets_to_backup(zfs_command, patterns): ) ), ) - if any( + if dataset.auto_backup + or any( pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT and pattern.source == borgmatic.borg.pattern.Pattern_source.CONFIG for pattern in contained_patterns @@ -260,7 +263,9 @@ def dump_data_sources( snapshot_mount_path = os.path.join( normalized_runtime_directory, 'zfs_snapshots', - hashlib.shake_256(dataset.mount_point.encode('utf-8')).hexdigest(MOUNT_POINT_HASH_LENGTH), + hashlib.shake_256(dataset.mount_point.encode('utf-8')).hexdigest( + MOUNT_POINT_HASH_LENGTH + ), dataset.mount_point.lstrip(os.path.sep), ) @@ -369,6 +374,7 @@ def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, d umount_command = hook_config.get('umount_command', 'umount') for snapshots_directory in glob.glob(snapshots_glob): + print('*** snapshots_glob:', snapshots_glob, 'snapshots_directory:', snapshots_directory) if not os.path.isdir(snapshots_directory): continue @@ -376,6 +382,7 @@ def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, d # child datasets before the shorter mount point paths of parent datasets. for mount_point in reversed(dataset_mount_points): snapshot_mount_path = os.path.join(snapshots_directory, mount_point.lstrip(os.path.sep)) + print('*** snapshot_mount_path:', snapshot_mount_path) if not os.path.isdir(snapshot_mount_path): continue @@ -397,7 +404,7 @@ def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, d unmount_snapshot(umount_command, snapshot_mount_path) except FileNotFoundError: logger.debug(f'Could not find "{umount_command}" command') - continue + return except subprocess.CalledProcessError as error: logger.debug(error) continue diff --git a/tests/end-to-end/commands/fake_zfs.py b/tests/end-to-end/commands/fake_zfs.py index 61f8ae5e..3c13f4d5 100644 --- a/tests/end-to-end/commands/fake_zfs.py +++ b/tests/end-to-end/commands/fake_zfs.py @@ -27,6 +27,7 @@ BUILTIN_DATASETS = ( 'used': '256K', 'avail': '23.7M', 'refer': '25K', + 'canmount': 'on', 'mountpoint': '/pool', }, { @@ -34,6 +35,7 @@ BUILTIN_DATASETS = ( 'used': '256K', 'avail': '23.7M', 'refer': '25K', + 'canmount': 'on', 'mountpoint': '/pool/dataset', }, ) diff --git a/tests/unit/hooks/data_source/test_btrfs.py b/tests/unit/hooks/data_source/test_btrfs.py index dde7b05b..65c4ccbe 100644 --- a/tests/unit/hooks/data_source/test_btrfs.py +++ b/tests/unit/hooks/data_source/test_btrfs.py @@ -52,9 +52,14 @@ def test_get_subvolume_mount_points_with_findmnt_json_missing_filesystems_errors def test_get_subvolumes_collects_subvolumes_matching_patterns(): flexmock(module).should_receive('get_subvolume_mount_points').and_return(('/mnt1', '/mnt2')) + contained_pattern = Pattern( + '/mnt1', + type=module.borgmatic.borg.pattern.Pattern_type.ROOT, + source=module.borgmatic.borg.pattern.Pattern_source.CONFIG, + ) flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive( 'get_contained_patterns' - ).with_args('/mnt1', object).and_return((Pattern('/mnt1'),)) + ).with_args('/mnt1', object).and_return((contained_pattern,)) flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive( 'get_contained_patterns' ).with_args('/mnt2', object).and_return(()) @@ -66,7 +71,69 @@ def test_get_subvolumes_collects_subvolumes_matching_patterns(): Pattern('/mnt1'), Pattern('/mnt3'), ], - ) == (module.Subvolume('/mnt1', contained_patterns=(Pattern('/mnt1'),)),) + ) == (module.Subvolume('/mnt1', contained_patterns=(contained_pattern,)),) + + +def test_get_subvolumes_skips_non_root_patterns(): + flexmock(module).should_receive('get_subvolume_mount_points').and_return(('/mnt1', '/mnt2')) + + flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive( + 'get_contained_patterns' + ).with_args('/mnt1', object).and_return( + ( + Pattern( + '/mnt1', + type=module.borgmatic.borg.pattern.Pattern_type.EXCLUDE, + source=module.borgmatic.borg.pattern.Pattern_source.CONFIG, + ), + ) + ) + flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive( + 'get_contained_patterns' + ).with_args('/mnt2', object).and_return(()) + + assert ( + module.get_subvolumes( + 'btrfs', + 'findmnt', + patterns=[ + Pattern('/mnt1'), + Pattern('/mnt3'), + ], + ) + == () + ) + + +def test_get_subvolumes_skips_non_config_patterns(): + flexmock(module).should_receive('get_subvolume_mount_points').and_return(('/mnt1', '/mnt2')) + + flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive( + 'get_contained_patterns' + ).with_args('/mnt1', object).and_return( + ( + Pattern( + '/mnt1', + type=module.borgmatic.borg.pattern.Pattern_type.ROOT, + source=module.borgmatic.borg.pattern.Pattern_source.HOOK, + ), + ) + ) + flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive( + 'get_contained_patterns' + ).with_args('/mnt2', object).and_return(()) + + assert ( + module.get_subvolumes( + 'btrfs', + 'findmnt', + patterns=[ + Pattern('/mnt1'), + Pattern('/mnt3'), + ], + ) + == () + ) def test_get_subvolumes_without_patterns_collects_all_subvolumes(): diff --git a/tests/unit/hooks/data_source/test_zfs.py b/tests/unit/hooks/data_source/test_zfs.py index 85782bdb..7bc07122 100644 --- a/tests/unit/hooks/data_source/test_zfs.py +++ b/tests/unit/hooks/data_source/test_zfs.py @@ -11,11 +11,19 @@ def test_get_datasets_to_backup_filters_datasets_by_patterns(): flexmock(module.borgmatic.execute).should_receive( 'execute_command_and_capture_output' ).and_return( - 'dataset\t/dataset\t-\nother\t/other\t-', + 'dataset\t/dataset\ton\t-\nother\t/other\ton\t-', ) flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive( 'get_contained_patterns' - ).with_args('/dataset', object).and_return((Pattern('/dataset'),)) + ).with_args('/dataset', object).and_return( + ( + Pattern( + '/dataset', + module.borgmatic.borg.pattern.Pattern_type.ROOT, + source=module.borgmatic.borg.pattern.Pattern_source.CONFIG, + ), + ) + ) flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive( 'get_contained_patterns' ).with_args('/other', object).and_return(()) @@ -23,24 +31,128 @@ def test_get_datasets_to_backup_filters_datasets_by_patterns(): assert module.get_datasets_to_backup( 'zfs', patterns=( - Pattern('/foo'), - Pattern('/dataset'), - Pattern('/bar'), + Pattern( + '/foo', + module.borgmatic.borg.pattern.Pattern_type.ROOT, + source=module.borgmatic.borg.pattern.Pattern_source.CONFIG, + ), + Pattern( + '/dataset', + module.borgmatic.borg.pattern.Pattern_type.ROOT, + source=module.borgmatic.borg.pattern.Pattern_source.CONFIG, + ), + Pattern( + '/bar', + module.borgmatic.borg.pattern.Pattern_type.ROOT, + source=module.borgmatic.borg.pattern.Pattern_source.CONFIG, + ), ), ) == ( module.Dataset( name='dataset', mount_point='/dataset', - contained_patterns=(Pattern('/dataset'),), + contained_patterns=( + Pattern( + '/dataset', + module.borgmatic.borg.pattern.Pattern_type.ROOT, + source=module.borgmatic.borg.pattern.Pattern_source.CONFIG, + ), + ), ), ) +def test_get_datasets_to_backup_skips_non_root_patterns(): + flexmock(module.borgmatic.execute).should_receive( + 'execute_command_and_capture_output' + ).and_return( + 'dataset\t/dataset\ton\t-\nother\t/other\ton\t-', + ) + flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive( + 'get_contained_patterns' + ).with_args('/dataset', object).and_return( + ( + Pattern( + '/dataset', + module.borgmatic.borg.pattern.Pattern_type.EXCLUDE, + source=module.borgmatic.borg.pattern.Pattern_source.CONFIG, + ), + ) + ) + flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive( + 'get_contained_patterns' + ).with_args('/other', object).and_return(()) + + assert module.get_datasets_to_backup( + 'zfs', + patterns=( + Pattern( + '/foo', + module.borgmatic.borg.pattern.Pattern_type.ROOT, + source=module.borgmatic.borg.pattern.Pattern_source.CONFIG, + ), + Pattern( + '/dataset', + module.borgmatic.borg.pattern.Pattern_type.EXCLUDE, + source=module.borgmatic.borg.pattern.Pattern_source.CONFIG, + ), + Pattern( + '/bar', + module.borgmatic.borg.pattern.Pattern_type.ROOT, + source=module.borgmatic.borg.pattern.Pattern_source.CONFIG, + ), + ), + ) == () + + +def test_get_datasets_to_backup_skips_non_config_patterns(): + flexmock(module.borgmatic.execute).should_receive( + 'execute_command_and_capture_output' + ).and_return( + 'dataset\t/dataset\ton\t-\nother\t/other\ton\t-', + ) + flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive( + 'get_contained_patterns' + ).with_args('/dataset', object).and_return( + ( + Pattern( + '/dataset', + module.borgmatic.borg.pattern.Pattern_type.ROOT, + source=module.borgmatic.borg.pattern.Pattern_source.HOOK, + ), + ) + ) + flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive( + 'get_contained_patterns' + ).with_args('/other', object).and_return(()) + + assert module.get_datasets_to_backup( + 'zfs', + patterns=( + Pattern( + '/foo', + module.borgmatic.borg.pattern.Pattern_type.ROOT, + source=module.borgmatic.borg.pattern.Pattern_source.CONFIG, + ), + Pattern( + '/dataset', + module.borgmatic.borg.pattern.Pattern_type.ROOT, + source=module.borgmatic.borg.pattern.Pattern_source.HOOK, + ), + Pattern( + '/bar', + module.borgmatic.borg.pattern.Pattern_type.ROOT, + source=module.borgmatic.borg.pattern.Pattern_source.CONFIG, + ), + ), + ) == () + + def test_get_datasets_to_backup_filters_datasets_by_user_property(): flexmock(module.borgmatic.execute).should_receive( 'execute_command_and_capture_output' ).and_return( - 'dataset\t/dataset\tauto\nother\t/other\t-', + 'dataset\t/dataset\ton\tauto\nother\t/other\ton\t-', ) flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive( 'get_contained_patterns' @@ -49,16 +161,47 @@ def test_get_datasets_to_backup_filters_datasets_by_user_property(): 'get_contained_patterns' ).with_args('/other', object).and_return(()) - assert module.get_datasets_to_backup('zfs', patterns=(Pattern('/foo'), Pattern('/bar'))) == ( + assert module.get_datasets_to_backup( + 'zfs', + patterns=(Pattern('/foo'), Pattern('/bar')), + ) == ( module.Dataset( name='dataset', mount_point='/dataset', auto_backup=True, - contained_patterns=(Pattern('/dataset'),), + contained_patterns=( + Pattern('/dataset', source=module.borgmatic.borg.pattern.Pattern_source.HOOK), + ), ), ) +def test_get_datasets_to_backup_filters_datasets_by_canmount_property(): + flexmock(module.borgmatic.execute).should_receive( + 'execute_command_and_capture_output' + ).and_return( + 'dataset\t/dataset\toff\t-\nother\t/other\ton\t-', + ) + flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive( + 'get_contained_patterns' + ).with_args('/dataset', object).and_return((Pattern('/dataset'),)) + flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive( + 'get_contained_patterns' + ).with_args('/other', object).and_return(()) + + assert ( + module.get_datasets_to_backup( + 'zfs', + patterns=( + Pattern('/foo'), + Pattern('/dataset'), + Pattern('/bar'), + ), + ) + == () + ) + + def test_get_datasets_to_backup_with_invalid_list_output_raises(): flexmock(module.borgmatic.execute).should_receive( 'execute_command_and_capture_output' @@ -94,13 +237,13 @@ def test_get_all_dataset_mount_points_does_not_filter_datasets(): ( ( Pattern('/foo/bar/baz'), - Pattern('/run/borgmatic/zfs_snapshots/./foo/bar/baz'), + Pattern('/run/borgmatic/zfs_snapshots/b33f/./foo/bar/baz'), ), - (Pattern('/foo/bar'), Pattern('/run/borgmatic/zfs_snapshots/./foo/bar')), + (Pattern('/foo/bar'), Pattern('/run/borgmatic/zfs_snapshots/b33f/./foo/bar')), ( Pattern('^/foo/bar', Pattern_type.INCLUDE, Pattern_style.REGULAR_EXPRESSION), Pattern( - '^/run/borgmatic/zfs_snapshots/./foo/bar', + '^/run/borgmatic/zfs_snapshots/b33f/./foo/bar', Pattern_type.INCLUDE, Pattern_style.REGULAR_EXPRESSION, ), @@ -108,46 +251,55 @@ def test_get_all_dataset_mount_points_does_not_filter_datasets(): ( Pattern('/foo/bar', Pattern_type.INCLUDE, Pattern_style.REGULAR_EXPRESSION), Pattern( - '/run/borgmatic/zfs_snapshots/./foo/bar', + '/run/borgmatic/zfs_snapshots/b33f/./foo/bar', Pattern_type.INCLUDE, Pattern_style.REGULAR_EXPRESSION, ), ), - (Pattern('/foo'), Pattern('/run/borgmatic/zfs_snapshots/./foo')), - (Pattern('/'), Pattern('/run/borgmatic/zfs_snapshots/./')), + (Pattern('/foo'), Pattern('/run/borgmatic/zfs_snapshots/b33f/./foo')), + (Pattern('/'), Pattern('/run/borgmatic/zfs_snapshots/b33f/./')), ), ) def test_make_borg_snapshot_pattern_includes_slashdot_hack_and_stripped_pattern_path( pattern, expected_pattern ): - assert module.make_borg_snapshot_pattern(pattern, '/run/borgmatic') == expected_pattern + flexmock(module.hashlib).should_receive('shake_256').and_return( + flexmock(hexdigest=lambda length: 'b33f') + ) + + assert ( + module.make_borg_snapshot_pattern( + pattern, flexmock(mount_point='/something'), '/run/borgmatic' + ) + == expected_pattern + ) def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns(): - flexmock(module).should_receive('get_datasets_to_backup').and_return( - ( - flexmock( - name='dataset', - mount_point='/mnt/dataset', - contained_patterns=(Pattern('/mnt/dataset/subdir'),), - ) - ) + dataset = flexmock( + name='dataset', + mount_point='/mnt/dataset', + contained_patterns=(Pattern('/mnt/dataset/subdir'),), ) + flexmock(module).should_receive('get_datasets_to_backup').and_return((dataset,)) flexmock(module.os).should_receive('getpid').and_return(1234) full_snapshot_name = 'dataset@borgmatic-1234' flexmock(module).should_receive('snapshot_dataset').with_args( 'zfs', full_snapshot_name, ).once() - snapshot_mount_path = '/run/borgmatic/zfs_snapshots/./mnt/dataset' + flexmock(module.hashlib).should_receive('shake_256').and_return( + flexmock(hexdigest=lambda length: 'b33f') + ) + snapshot_mount_path = '/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset' flexmock(module).should_receive('mount_snapshot').with_args( 'mount', full_snapshot_name, module.os.path.normpath(snapshot_mount_path), ).once() flexmock(module).should_receive('make_borg_snapshot_pattern').with_args( - Pattern('/mnt/dataset/subdir'), '/run/borgmatic' - ).and_return(Pattern('/run/borgmatic/zfs_snapshots/./mnt/dataset/subdir')) + Pattern('/mnt/dataset/subdir'), dataset, '/run/borgmatic' + ).and_return(Pattern('/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir')) patterns = [Pattern('/mnt/dataset/subdir')] assert ( @@ -188,30 +340,30 @@ def test_dump_data_sources_with_no_datasets_skips_snapshots(): def test_dump_data_sources_uses_custom_commands(): - flexmock(module).should_receive('get_datasets_to_backup').and_return( - ( - flexmock( - name='dataset', - mount_point='/mnt/dataset', - contained_patterns=(Pattern('/mnt/dataset/subdir'),), - ) - ) + dataset = flexmock( + name='dataset', + mount_point='/mnt/dataset', + contained_patterns=(Pattern('/mnt/dataset/subdir'),), ) + flexmock(module).should_receive('get_datasets_to_backup').and_return((dataset,)) flexmock(module.os).should_receive('getpid').and_return(1234) full_snapshot_name = 'dataset@borgmatic-1234' flexmock(module).should_receive('snapshot_dataset').with_args( '/usr/local/bin/zfs', full_snapshot_name, ).once() - snapshot_mount_path = '/run/borgmatic/zfs_snapshots/./mnt/dataset' + flexmock(module.hashlib).should_receive('shake_256').and_return( + flexmock(hexdigest=lambda length: 'b33f') + ) + snapshot_mount_path = '/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset' flexmock(module).should_receive('mount_snapshot').with_args( '/usr/local/bin/mount', full_snapshot_name, module.os.path.normpath(snapshot_mount_path), ).once() flexmock(module).should_receive('make_borg_snapshot_pattern').with_args( - Pattern('/mnt/dataset/subdir'), '/run/borgmatic' - ).and_return(Pattern('/run/borgmatic/zfs_snapshots/./mnt/dataset/subdir')) + Pattern('/mnt/dataset/subdir'), dataset, '/run/borgmatic' + ).and_return(Pattern('/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir')) patterns = [Pattern('/mnt/dataset/subdir')] hook_config = { 'zfs_command': '/usr/local/bin/zfs', @@ -261,30 +413,30 @@ def test_dump_data_sources_with_dry_run_skips_commands_and_does_not_touch_patter def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained_patterns(): - flexmock(module).should_receive('get_datasets_to_backup').and_return( - ( - flexmock( - name='dataset', - mount_point='/mnt/dataset', - contained_patterns=(Pattern('/mnt/dataset/subdir'),), - ) - ) + dataset = flexmock( + name='dataset', + mount_point='/mnt/dataset', + contained_patterns=(Pattern('/mnt/dataset/subdir'),), ) + flexmock(module).should_receive('get_datasets_to_backup').and_return((dataset,)) flexmock(module.os).should_receive('getpid').and_return(1234) full_snapshot_name = 'dataset@borgmatic-1234' flexmock(module).should_receive('snapshot_dataset').with_args( 'zfs', full_snapshot_name, ).once() - snapshot_mount_path = '/run/borgmatic/zfs_snapshots/./mnt/dataset' + flexmock(module.hashlib).should_receive('shake_256').and_return( + flexmock(hexdigest=lambda length: 'b33f') + ) + snapshot_mount_path = '/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset' flexmock(module).should_receive('mount_snapshot').with_args( 'mount', full_snapshot_name, module.os.path.normpath(snapshot_mount_path), ).once() flexmock(module).should_receive('make_borg_snapshot_pattern').with_args( - Pattern('/mnt/dataset/subdir'), '/run/borgmatic' - ).and_return(Pattern('/run/borgmatic/zfs_snapshots/./mnt/dataset/subdir')) + Pattern('/mnt/dataset/subdir'), dataset, '/run/borgmatic' + ).and_return(Pattern('/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir')) patterns = [Pattern('/hmm')] assert ( @@ -317,11 +469,13 @@ def test_remove_data_source_dumps_unmounts_and_destroys_snapshots(): 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.glob).should_receive('glob').replace_with( + lambda path: [path.replace('*', 'b33f')] + ) flexmock(module.os.path).should_receive('isdir').and_return(True) flexmock(module.shutil).should_receive('rmtree') flexmock(module).should_receive('unmount_snapshot').with_args( - 'umount', '/run/borgmatic/zfs_snapshots/mnt/dataset' + 'umount', '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset' ).once() flexmock(module).should_receive('get_all_snapshots').and_return( ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid') @@ -343,11 +497,13 @@ def test_remove_data_source_dumps_use_custom_commands(): 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.glob).should_receive('glob').replace_with( + lambda path: [path.replace('*', 'b33f')] + ) flexmock(module.os.path).should_receive('isdir').and_return(True) flexmock(module.shutil).should_receive('rmtree') flexmock(module).should_receive('unmount_snapshot').with_args( - '/usr/local/bin/umount', '/run/borgmatic/zfs_snapshots/mnt/dataset' + '/usr/local/bin/umount', '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset' ).once() flexmock(module).should_receive('get_all_snapshots').and_return( ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid') @@ -416,11 +572,13 @@ def test_remove_data_source_dumps_bails_for_missing_umount_command(): 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.glob).should_receive('glob').replace_with( + lambda path: [path.replace('*', 'b33f')] + ) flexmock(module.os.path).should_receive('isdir').and_return(True) flexmock(module.shutil).should_receive('rmtree') flexmock(module).should_receive('unmount_snapshot').with_args( - '/usr/local/bin/umount', '/run/borgmatic/zfs_snapshots/mnt/dataset' + '/usr/local/bin/umount', '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset' ).and_raise(FileNotFoundError) flexmock(module).should_receive('get_all_snapshots').never() flexmock(module).should_receive('destroy_snapshot').never() @@ -434,19 +592,25 @@ def test_remove_data_source_dumps_bails_for_missing_umount_command(): ) -def test_remove_data_source_dumps_bails_for_umount_command_error(): +def test_remove_data_source_dumps_swallows_umount_command_error(): flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',)) 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.glob).should_receive('glob').replace_with( + lambda path: [path.replace('*', 'b33f')] + ) flexmock(module.os.path).should_receive('isdir').and_return(True) flexmock(module.shutil).should_receive('rmtree') flexmock(module).should_receive('unmount_snapshot').with_args( - '/usr/local/bin/umount', '/run/borgmatic/zfs_snapshots/mnt/dataset' + '/usr/local/bin/umount', '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset' ).and_raise(module.subprocess.CalledProcessError(1, 'wtf')) - flexmock(module).should_receive('get_all_snapshots').never() - flexmock(module).should_receive('destroy_snapshot').never() + flexmock(module).should_receive('get_all_snapshots').and_return( + ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid') + ) + flexmock(module).should_receive('destroy_snapshot').with_args( + '/usr/local/bin/zfs', 'dataset@borgmatic-1234' + ).once() hook_config = {'zfs_command': '/usr/local/bin/zfs', 'umount_command': '/usr/local/bin/umount'} module.remove_data_source_dumps( @@ -462,7 +626,9 @@ def test_remove_data_source_dumps_skips_unmount_snapshot_directories_that_are_no 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.glob).should_receive('glob').replace_with( + lambda path: [path.replace('*', 'b33f')] + ) flexmock(module.os.path).should_receive('isdir').and_return(False) flexmock(module.shutil).should_receive('rmtree').never() flexmock(module).should_receive('unmount_snapshot').never() @@ -486,12 +652,14 @@ def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_that_are_no 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.glob).should_receive('glob').replace_with( + lambda path: [path.replace('*', 'b33f')] + ) flexmock(module.os.path).should_receive('isdir').with_args( - '/run/borgmatic/zfs_snapshots' + '/run/borgmatic/zfs_snapshots/b33f' ).and_return(True) flexmock(module.os.path).should_receive('isdir').with_args( - '/run/borgmatic/zfs_snapshots/mnt/dataset' + '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset' ).and_return(False) flexmock(module.shutil).should_receive('rmtree') flexmock(module).should_receive('unmount_snapshot').never() @@ -515,12 +683,14 @@ def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_after_rmtre 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.glob).should_receive('glob').replace_with( + lambda path: [path.replace('*', 'b33f')] + ) flexmock(module.os.path).should_receive('isdir').with_args( - '/run/borgmatic/zfs_snapshots' + '/run/borgmatic/zfs_snapshots/b33f' ).and_return(True) flexmock(module.os.path).should_receive('isdir').with_args( - '/run/borgmatic/zfs_snapshots/mnt/dataset' + '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset' ).and_return(True).and_return(False) flexmock(module.shutil).should_receive('rmtree') flexmock(module).should_receive('unmount_snapshot').never() @@ -544,7 +714,9 @@ def test_remove_data_source_dumps_with_dry_run_skips_unmount_and_destroy(): 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.glob).should_receive('glob').replace_with( + lambda path: [path.replace('*', 'b33f')] + ) flexmock(module.os.path).should_receive('isdir').and_return(True) flexmock(module.shutil).should_receive('rmtree').never() flexmock(module).should_receive('unmount_snapshot').never()