]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
qa: add charmap tests
authorPatrick Donnelly <pdonnell@ibm.com>
Tue, 31 Dec 2024 17:59:01 +0000 (12:59 -0500)
committerPatrick Donnelly <pdonnell@ibm.com>
Mon, 17 Mar 2025 19:43:21 +0000 (15:43 -0400)
Signed-off-by: Patrick Donnelly <pdonnell@ibm.com>
(cherry picked from commit 84bf1898be53dcb6a088b6231c7a845ea4981dca)

qa/suites/fs/functional/tasks/charmap.yaml [new file with mode: 0644]
qa/tasks/cephfs/test_dir_charmap.py [new file with mode: 0644]

diff --git a/qa/suites/fs/functional/tasks/charmap.yaml b/qa/suites/fs/functional/tasks/charmap.yaml
new file mode 100644 (file)
index 0000000..a572530
--- /dev/null
@@ -0,0 +1,9 @@
+# charmap is only supported by the userspace Client at this time
+teuthology:
+  postmerge:
+  - if not is_fuse() then reject() end
+tasks:
+  - cephfs_test_runner:
+      fail_on_skip: false
+      modules:
+        - tasks.cephfs.test_dir_charmap
diff --git a/qa/tasks/cephfs/test_dir_charmap.py b/qa/tasks/cephfs/test_dir_charmap.py
new file mode 100644 (file)
index 0000000..e4b52bc
--- /dev/null
@@ -0,0 +1,370 @@
+import base64
+import json
+from logging import getLogger
+
+from teuthology.exceptions import CommandFailedError
+from tasks.cephfs.cephfs_test_case import CephFSTestCase
+from tasks.cephfs.mount import NoSuchAttributeError, InvalidArgumentError, DirectoryNotEmptyError
+
+log = getLogger(__name__)
+
+class CharMapMixin:
+    def check_cs(self, path, **kwargs):
+        what = kwargs
+        what.setdefault("casesensitive", True)
+        what.setdefault("normalization", "nfd")
+        what.setdefault("encoding", "utf8")
+        v = self.mount_a.getfattr(path, "ceph.dir.charmap", helpfulexception=True)
+        J = json.loads(v)
+        log.debug("cs = %s", v)
+        self.assertEqual(what, J)
+
+class TestCharMapVxattr(CephFSTestCase, CharMapMixin):
+    CLIENTS_REQUIRED = 1
+    MDSS_REQUIRED = 1
+
+    def test_cs_get_charmap_none(self):
+        """
+        That getvxattr for a charmap fails if not present in Inode.
+        """
+        self.mount_a.run_shell_payload("mkdir foo/")
+        try:
+            self.check_cs("foo")
+        except NoSuchAttributeError:
+            pass
+        else:
+            self.fail("should raise error")
+
+    def test_cs_get_charmap_set(self):
+        """
+        That setvxattr fails for charmap.
+        """
+        self.mount_a.run_shell_payload("mkdir foo/")
+        try:
+            self.mount_a.setfattr("foo/", "ceph.dir.charmap", "0", helpfulexception=True)
+        except InvalidArgumentError:
+            pass
+        else:
+            self.fail("should raise error")
+
+    def test_cs_set_charmap_inherited(self):
+        """
+        That charmap is inherited.
+        """
+        self.mount_a.run_shell_payload("mkdir foo/")
+        self.mount_a.setfattr("foo/", "ceph.dir.casesensitive", "0")
+        self.mount_a.run_shell_payload("mkdir foo/bar")
+        self.check_cs("foo/bar/", casesensitive=False)
+
+    def test_cs_get_charmap_none_rm(self):
+        """
+        That rmvxattr actually removes the metadata from the Inode.
+        """
+        self.mount_a.run_shell_payload("mkdir foo/")
+        self.mount_a.setfattr("foo/", "ceph.dir.casesensitive", "0")
+        self.mount_a.removexattr("foo/", "ceph.dir.charmap")
+        try:
+            self.check_cs("foo")
+        except NoSuchAttributeError:
+            pass
+        else:
+            self.fail("should raise error")
+
+    def test_cs_get_charmap_none_dup(self):
+        """
+        That rmvxattr is idempotent.
+        """
+        self.mount_a.run_shell_payload("mkdir foo/")
+        self.mount_a.setfattr("foo/", "ceph.dir.casesensitive", "0")
+        self.mount_a.removexattr("foo/", "ceph.dir.charmap")
+        self.mount_a.removexattr("foo/", "ceph.dir.charmap")
+
+    def test_cs_set_encoding_valid(self):
+        """
+        That we can set ceph.dir.encoding and check it.
+        """
+        self.mount_a.run_shell_payload("mkdir foo/")
+        self.mount_a.setfattr("foo/", "ceph.dir.encoding", "utf16")
+        self.check_cs("foo", encoding="utf16")
+        self.mount_a.setfattr("foo/", "ceph.dir.encoding", "utf8")
+        self.check_cs("foo", encoding="utf8")
+
+    def test_cs_set_encoding_garbage(self):
+        """
+        That a garbage encoding is accepted but prevents creating any dentries.
+        """
+        self.mount_a.run_shell_payload("mkdir foo/")
+        self.mount_a.setfattr("foo/", "ceph.dir.encoding", "garbage")
+        self.check_cs("foo", encoding="garbage")
+        try:
+            p = self.mount_a.run_shell_payload("mkdir foo/test", wait=False)
+            p.wait()
+        except CommandFailedError:
+            stderr = p.stderr.getvalue()
+            self.assertIn("Permission denied", stderr)
+        else:
+            self.fail("should fail")
+
+    def test_cs_rm_encoding(self):
+        """
+        That removing the encoding without any other charmappings will restore access.
+        """
+        self.mount_a.run_shell_payload("mkdir foo/")
+        self.mount_a.setfattr("foo/", "ceph.dir.encoding", "garbage")
+        self.mount_a.removexattr("foo/", "ceph.dir.encoding")
+        self.check_cs("foo")
+        self.mount_a.run_shell_payload("mkdir foo/test")
+
+    def test_cs_set_insensitive_valid(self):
+        """
+        That we can set ceph.dir.casesensitive and check it.
+        """
+        self.mount_a.run_shell_payload("mkdir foo/")
+        self.mount_a.setfattr("foo/", "ceph.dir.casesensitive", "0")
+        self.check_cs("foo", casesensitive=False)
+        self.mount_a.setfattr("foo/", "ceph.dir.casesensitive", "1")
+        self.check_cs("foo")
+
+    def test_cs_set_insensitive_garbage(self):
+        """
+        That setting ceph.dir.casesensitive to garbage is rejected (should be bool).
+        """
+        self.mount_a.run_shell_payload("mkdir foo/")
+        try:
+            self.mount_a.setfattr("foo/", "ceph.dir.casesensitive", "abc", helpfulexception=True)
+        except InvalidArgumentError:
+            pass
+        else:
+            self.fail("should fail")
+        try:
+            self.check_cs("foo")
+        except NoSuchAttributeError:
+            pass
+        else:
+            self.fail("should raise error")
+
+    def test_cs_rm_insensitive(self):
+        """
+        That we can remove ceph.dir.casesensitive and restore the default.
+        """
+        self.mount_a.run_shell_payload("mkdir foo/")
+        self.mount_a.setfattr("foo/", "ceph.dir.casesensitive", "0")
+        self.mount_a.removexattr("foo/", "ceph.dir.casesensitive")
+        self.check_cs("foo")
+
+    def test_cs_set_normalization(self):
+        """
+        That we can set ceph.dir.normalization and check it.
+        """
+        self.mount_a.run_shell_payload("mkdir foo/")
+        self.mount_a.setfattr("foo/", "ceph.dir.normalization", "nfc")
+        self.check_cs("foo", normalization="nfc")
+        self.mount_a.setfattr("foo/", "ceph.dir.normalization", "nfd")
+        self.check_cs("foo", normalization="nfd")
+
+    def test_cs_set_normalization_garbage(self):
+        """
+        That a garbage normalization is accepted but prevents creating any dentries.
+        """
+        self.mount_a.run_shell_payload("mkdir foo/")
+        self.mount_a.setfattr("foo/", "ceph.dir.normalization", "abc")
+        self.check_cs("foo", normalization="abc")
+        try:
+            p = self.mount_a.run_shell_payload("mkdir foo/test", wait=False)
+            p.wait()
+        except CommandFailedError:
+            stderr = p.stderr.getvalue()
+            self.assertIn("Permission denied", stderr)
+        else:
+            self.fail("should fail")
+
+    def test_cs_feature_bit(self):
+        """
+        That the CEPHFS_FEATURE_CHARMAP feature bit enforces access.
+        """
+
+        self.mount_a.run_shell_payload("mkdir foo/")
+        self.mount_a.setfattr("foo/", "ceph.dir.casesensitive", "0")
+        self.check_cs("foo", casesensitive=False)
+
+        self.mount_a.run_shell_payload("dd if=/dev/urandom of=foo/Test1 bs=4k count=1")
+
+        CEPHFS_FEATURE_CHARMAP = 22
+        # all but CEPHFS_FEATURE_CHARMAP
+        features = ",".join([str(i) for i in range(CEPHFS_FEATURE_CHARMAP)])
+        mntargs = [f"--client_debug_inject_features={features}"]
+
+        self.mount_a.remount(mntargs=mntargs)
+
+        self.check_cs("foo", casesensitive=False)
+
+        cmds = [
+          "mkdir foo/test2",
+          "ln -s . foo/test2",
+          "ln foo/Test1 foo/test2",
+          "dd if=/dev/urandom of=foo/test2 bs=4k count=1",
+          "mv foo/Test1 foo/Test2",
+        ]
+        for cmd in cmds:
+            try:
+                p = self.mount_a.run_shell_payload(cmd, wait=False)
+                p.wait()
+            except CommandFailedError:
+                stderr = p.stderr.getvalue()
+                self.assertIn("Operation not permitted", stderr)
+            else:
+                self.fail("should fail")
+
+        okay_cmds = [
+          "ls foo/",
+          "stat foo/test1",
+          "rm foo/test1",
+        ]
+        for cmd in okay_cmds:
+            try:
+                p = self.mount_a.run_shell_payload(cmd, wait=False)
+                p.wait()
+            except CommandFailedError:
+                stderr = p.stderr.getvalue()
+                self.fail("command failed:\n%s", stderr)
+
+
+    def test_cs_remount(self):
+        """
+        That a remount continues to see the charmap.
+        """
+
+        self.mount_a.run_shell_payload("mkdir foo/")
+        self.mount_a.setfattr("foo/", "ceph.dir.casesensitive", "0")
+        self.check_cs("foo", casesensitive=False)
+
+        self.mount_a.umount_wait()
+        self.mount_a.mount()
+
+        self.check_cs("foo", casesensitive=False)
+
+    def test_cs_not_empty_set_insensitive(self):
+        """
+        That setting a charmap fails for a non-empty directory.
+        """
+
+        attrs = {
+          "ceph.dir.casesensitive": "0",
+          "ceph.dir.normalization": "nfc",
+          "ceph.dir.encoding": "utf8",
+        }
+
+        self.mount_a.run_shell_payload("mkdir -p foo/dir")
+        for attr, v in attrs.items():
+            try:
+                self.mount_a.setfattr("foo/", attr, v, helpfulexception=True)
+            except DirectoryNotEmptyError:
+                pass
+            else:
+                self.fail("should fail")
+            try:
+                self.check_cs("foo")
+            except NoSuchAttributeError:
+                pass
+            else:
+                self.fail("should fail")
+
+class TestNormalization(CephFSTestCase, CharMapMixin):
+    """
+    Test charmap normalization.
+    """
+
+    def test_normalization(self):
+        """
+        That a normalization works for a conventional example.
+        """
+
+        dname = "Grüßen"
+        self.mount_a.run_shell_payload("mkdir foo/")
+        self.mount_a.setfattr("foo/", "ceph.dir.normalization", "nfd") # default
+
+        self.mount_a.run_shell_payload(f"mkdir foo/{dname}")
+        c = self.fs.read_cache("foo", depth=0)
+
+        self.assertEqual(len(c), 1)
+        frags = c[0]['dirfrags']
+        self.assertEqual(len(frags), 1)
+        frag = frags[0]
+        dentries = frag['dentries']
+        self.assertEqual(len(dentries), 1)
+        dentry = dentries[0]
+        # ü to u + u0308
+        self.assertEqual(dentry['path'], "foo/Gru\u0308\u00dfen")
+        altn = dentry['alternate_name']
+        altn_bin = base64.b64decode(altn)
+        expected = bytes([0x47, 0x72, 0xc3, 0xbc, 0xc3, 0x9f, 0x65, 0x6e]) # 8 not 9 chars
+        self.assertIn(expected, altn_bin)
+
+class TestEncoding(CephFSTestCase, CharMapMixin):
+    """
+    Test charmap encoding.
+    """
+
+    def test_encoding(self):
+        """
+        That an encoding-only charmap still normalizes.
+        """
+
+        # N.B.: you cannot disable normalization. Setting to empty string
+        # restores default.
+
+        dname = "Grüßen"
+        self.mount_a.run_shell_payload("mkdir foo/")
+        self.mount_a.setfattr("foo/", "ceph.dir.encoding", "utf8")
+
+        self.mount_a.run_shell_payload(f"mkdir foo/{dname}")
+        c = self.fs.read_cache("foo", depth=0)
+
+        self.assertEqual(len(c), 1)
+        frags = c[0]['dirfrags']
+        self.assertEqual(len(frags), 1)
+        frag = frags[0]
+        dentries = frag['dentries']
+        self.assertEqual(len(dentries), 1)
+        dentry = dentries[0]
+        self.assertEqual(dentry['path'], "foo/Gru\u0308\u00dfen")
+        altn = dentry['alternate_name']
+        altn_bin = base64.b64decode(altn)
+        expected = bytes([0x47, 0x72, 0xc3, 0xbc, 0xc3, 0x9f, 0x65, 0x6e]) # 8 not 9 chars
+        self.assertIn(expected, altn_bin)
+
+
+class TestCaseFolding(CephFSTestCase, CharMapMixin):
+    """
+    Test charmap case folding.
+    """
+
+    def test_casefolding(self):
+        """
+        That a case folding works for a conventional example.
+        """
+
+        dname = "Grüßen"
+        self.mount_a.run_shell_payload("mkdir foo/")
+        self.mount_a.setfattr("foo/", "ceph.dir.casesensitive", "0")
+
+        self.mount_a.run_shell_payload(f"mkdir foo/{dname}")
+        c = self.fs.read_cache("foo", depth=0)
+
+        self.assertEqual(len(c), 1)
+        frags = c[0]['dirfrags']
+        self.assertEqual(len(frags), 1)
+        frag = frags[0]
+        dentries = frag['dentries']
+        self.assertEqual(len(dentries), 1)
+        dentry = dentries[0]
+        path = dentry['path'].encode('utf-8')
+        # Grüßen to Gru \u0308 ssen
+        # foo/gru\u0308ssen
+        expected = bytes([0x66, 0x6f, 0x6f, 0x2f, 0x67, 0x72, 0x75, 0xcc, 0x88, 0x73, 0x73, 0x65, 0x6e])
+        self.assertEqual(path, expected)
+        # Grüßen
+        altn = dentry['alternate_name']
+        altn_bin = base64.b64decode(altn)
+        expected = base64.b64decode("R3LDvMOfZW4=") # 8 chars, not 9
+        self.assertIn(expected, altn_bin)