Sfoglia il codice sorgente

Fix a "no such file or directory" error in ZFS, Btrfs, and LVM hooks with nested directories that reside on separate devices/filesystems (#1048).

Dan Helfman 2 mesi fa
parent
commit
ab01e97a5e

+ 2 - 0
NEWS

@@ -22,6 +22,8 @@
    "working_directory" are used.
  * #1044: Fix an error in the systemd credential hook when the credential name contains a "."
    character.
+ * #1048: Fix a "no such file or directory" error in ZFS, Btrfs, and LVM hooks with nested
+   directories that reside on separate devices/filesystems.
 
 1.9.14
  * #409: With the PagerDuty monitoring hook, send borgmatic logs to PagerDuty so they show up in the

+ 8 - 0
borgmatic/hooks/data_source/snapshot.py

@@ -1,3 +1,4 @@
+import os
 import pathlib
 
 IS_A_HOOK = False
@@ -11,6 +12,10 @@ def get_contained_patterns(parent_directory, candidate_patterns):
     paths, but there's a parent directory (logical volume, dataset, subvolume, etc.) at /var, then
     /var is what we want to snapshot.
 
+    If a parent directory and a candidate pattern are on different devices, skip the pattern. That's
+    because any snapshot of a parent directory won't actually include "contained" directories if
+    they reside on separate devices.
+
     For this function to work, a candidate pattern path can't have any globs or other non-literal
     characters in the initial portion of the path that matches the parent directory. For instance, a
     parent directory of /var would match a candidate pattern path of /var/log/*/data, but not a
@@ -27,6 +32,8 @@ def get_contained_patterns(parent_directory, candidate_patterns):
     if not candidate_patterns:
         return ()
 
+    parent_device = os.stat(parent_directory).st_dev
+
     contained_patterns = tuple(
         candidate
         for candidate in candidate_patterns
@@ -35,6 +42,7 @@ def get_contained_patterns(parent_directory, candidate_patterns):
             pathlib.PurePath(parent_directory) == candidate_path
             or pathlib.PurePath(parent_directory) in candidate_path.parents
         )
+        if candidate.device == parent_device
     )
     candidate_patterns -= set(contained_patterns)
 

+ 62 - 12
tests/unit/hooks/data_source/test_snapshot.py

@@ -1,34 +1,84 @@
+from flexmock import flexmock
+
 from borgmatic.borg.pattern import Pattern
 from borgmatic.hooks.data_source import snapshot as module
 
 
 def test_get_contained_patterns_without_candidates_returns_empty():
+    flexmock(module.os).should_receive('stat').and_return(flexmock(st_dev=flexmock()))
+
     assert module.get_contained_patterns('/mnt', {}) == ()
 
 
 def test_get_contained_patterns_with_self_candidate_returns_self():
-    candidates = {Pattern('/foo'), Pattern('/mnt'), Pattern('/bar')}
+    device = flexmock()
+    flexmock(module.os).should_receive('stat').and_return(flexmock(st_dev=device))
+    candidates = {
+        Pattern('/foo', device=device),
+        Pattern('/mnt', device=device),
+        Pattern('/bar', device=device),
+    }
 
-    assert module.get_contained_patterns('/mnt', candidates) == (Pattern('/mnt'),)
-    assert candidates == {Pattern('/foo'), Pattern('/bar')}
+    assert module.get_contained_patterns('/mnt', candidates) == (Pattern('/mnt', device=device),)
+    assert candidates == {Pattern('/foo', device=device), Pattern('/bar', device=device)}
 
 
 def test_get_contained_patterns_with_self_candidate_and_caret_prefix_returns_self():
-    candidates = {Pattern('^/foo'), Pattern('^/mnt'), Pattern('^/bar')}
+    device = flexmock()
+    flexmock(module.os).should_receive('stat').and_return(flexmock(st_dev=device))
+    candidates = {
+        Pattern('^/foo', device=device),
+        Pattern('^/mnt', device=device),
+        Pattern('^/bar', device=device),
+    }
 
-    assert module.get_contained_patterns('/mnt', candidates) == (Pattern('^/mnt'),)
-    assert candidates == {Pattern('^/foo'), Pattern('^/bar')}
+    assert module.get_contained_patterns('/mnt', candidates) == (Pattern('^/mnt', device=device),)
+    assert candidates == {Pattern('^/foo', device=device), Pattern('^/bar', device=device)}
 
 
 def test_get_contained_patterns_with_child_candidate_returns_child():
-    candidates = {Pattern('/foo'), Pattern('/mnt/subdir'), Pattern('/bar')}
+    device = flexmock()
+    flexmock(module.os).should_receive('stat').and_return(flexmock(st_dev=device))
+    candidates = {
+        Pattern('/foo', device=device),
+        Pattern('/mnt/subdir', device=device),
+        Pattern('/bar', device=device),
+    }
 
-    assert module.get_contained_patterns('/mnt', candidates) == (Pattern('/mnt/subdir'),)
-    assert candidates == {Pattern('/foo'), Pattern('/bar')}
+    assert module.get_contained_patterns('/mnt', candidates) == (
+        Pattern('/mnt/subdir', device=device),
+    )
+    assert candidates == {Pattern('/foo', device=device), Pattern('/bar', device=device)}
 
 
 def test_get_contained_patterns_with_grandchild_candidate_returns_child():
-    candidates = {Pattern('/foo'), Pattern('/mnt/sub/dir'), Pattern('/bar')}
+    device = flexmock()
+    flexmock(module.os).should_receive('stat').and_return(flexmock(st_dev=device))
+    candidates = {
+        Pattern('/foo', device=device),
+        Pattern('/mnt/sub/dir', device=device),
+        Pattern('/bar', device=device),
+    }
+
+    assert module.get_contained_patterns('/mnt', candidates) == (
+        Pattern('/mnt/sub/dir', device=device),
+    )
+    assert candidates == {Pattern('/foo', device=device), Pattern('/bar', device=device)}
+
+
+def test_get_contained_patterns_ignores_child_candidate_on_another_device():
+    one_device = flexmock()
+    another_device = flexmock()
+    flexmock(module.os).should_receive('stat').and_return(flexmock(st_dev=one_device))
+    candidates = {
+        Pattern('/foo', device=one_device),
+        Pattern('/mnt/subdir', device=another_device),
+        Pattern('/bar', device=one_device),
+    }
 
-    assert module.get_contained_patterns('/mnt', candidates) == (Pattern('/mnt/sub/dir'),)
-    assert candidates == {Pattern('/foo'), Pattern('/bar')}
+    assert module.get_contained_patterns('/mnt', candidates) == ()
+    assert candidates == {
+        Pattern('/foo', device=one_device),
+        Pattern('/mnt/subdir', device=another_device),
+        Pattern('/bar', device=one_device),
+    }