From 742a56f69c4efe9ff4c8b8acb7c704ff2b08d184 Mon Sep 17 00:00:00 2001 From: Christopher Hoffman Date: Mon, 12 May 2025 16:32:52 +0000 Subject: [PATCH] qa: Add tests for fscrypt subvolume Add various tests for fscrypt subvolumes such as snapshots and verifying clones. Signed-off-by: Christopher Hoffman --- qa/tasks/cephfs/test_fscrypt.py | 216 +++++++++++++++++++++++++++++++- 1 file changed, 215 insertions(+), 1 deletion(-) diff --git a/qa/tasks/cephfs/test_fscrypt.py b/qa/tasks/cephfs/test_fscrypt.py index 9d907a981f9..8ca7eb28ff5 100644 --- a/qa/tasks/cephfs/test_fscrypt.py +++ b/qa/tasks/cephfs/test_fscrypt.py @@ -1,11 +1,14 @@ from io import StringIO from os.path import basename +import json +import os import random import string import time +import hashlib from logging import getLogger - +from teuthology.contextutil import safe_while from tasks.cephfs.cephfs_test_case import CephFSTestCase from tasks.cephfs.xfstests_dev import XFSTestsDev @@ -359,6 +362,217 @@ class TestFSCryptRMW(FSCryptTestCase): self.strided_tests(fscrypt_block_size, write_size, num_writes, shared_file, fill) +class TestFSCryptVolumes(CephFSTestCase): + MDSS_REQUIRED = 2 + + def setUp(self): + super().setUp() + + self.protector = ''.join(random.choice(string.ascii_letters) for _ in range(8)) + self.key_file = "/tmp/key_volume" + self.path = "dir/" + + self.mount_a.run_shell_payload(f"sudo fscrypt setup {self.mount_a.hostfs_mntpt} --verbose") + self.mount_a.run_shell_payload("sudo fscrypt status --verbose") + self.mount_a.run_shell_payload(f"sudo dd if=/dev/urandom of={self.key_file} bs=32 count=1") + self.mount_a.run_shell_payload(f"mkdir -p {self.path}") + + def tearDown(self): + self.mount_a.run_shell_payload(f"sudo fscrypt purge --force --verbose {self.mount_a.hostfs_mntpt}") + super().tearDown() + + def _compare_trees(self, src, dst): + files = os.listdir(src) + for file in files: + src_file = f'{src}/{file}' + dst_file = f'{dst}/{file}' + + exists = os.path.exists(dst_file) + lexists = os.path.lexists(dst_file) + if not exists and not lexists: + log.debug(f'path_dne:={dst_file}') + raise + + if os.path.islink(src): + #do link check + if os.readlink(src_file) != os.readlink(dst_file): + raise + elif os.path.isfile(src): + #check reported size + rsize_match = os.path.getsize(src_file) == os.path.getsize(dst_file) + if not rsize_match: + raise + + #check contents + src_hash = self._md5hash_file(src_file) + dest_hash = self._md5hash_file(dst_file) + if src_hash != dest_hash: + raise + + def _md5hash_file(self, file): + md5 = hashlib.md5() + with open(file, "rb") as f: + md5.update(f.read()) + return md5.hexdigest() + + def _get_sv_path(self, v, sv): + sv_path = self.get_ceph_cmd_stdout(f'fs subvolume getpath {v} {sv}') + sv_path = sv_path.strip() + # delete slash at the beginning of path + sv_path = sv_path[1:] + + sv_path = os.path.join(self.mount_a.mountpoint, sv_path) + return sv_path + + def _fs_cmd(self, *args): + return self.get_ceph_cmd_stdout("fs", *args) + + def __check_clone_state(self, states, volname, clone, clone_group=None, timo=120): + if isinstance(states, str): + states = (states, ) + + args = ["clone", "status", volname, clone] + if clone_group: + args.append(clone_group) + args = tuple(args) + + msg = (f'Executed cmd "{args}" {timo} times; clone was never in ' + f'"{states}" state(s).') + + with safe_while(tries=timo, sleep=1, action=msg) as proceed: + while proceed(): + result = json.loads(self._fs_cmd(*args)) + current_state = result["status"]["state"] + + log.debug(f'current clone state = {current_state}') + if current_state in states: + return + + def _wait_for_clone_to_complete(self, volname, clone, clone_group=None, timo=120): + self.__check_clone_state("complete", volname, clone, clone_group, timo) + + def test_fscrypt_snap(self): + """ Test that snapshot names are not encrypted """ + + self.mount_a.run_shell_payload(f"sudo fscrypt encrypt --verbose --source=raw_key --name={self.protector} --no-recovery --key={self.key_file} {self.path}") + + snap_path = f'{self.path}/.snap/s1' + self.mount_a.run_shell_payload(f'mkdir -p {snap_path}') + self.mount_a.run_shell_payload(f'ls {snap_path}') + + self.mount_a.run_shell_payload(f"sudo fscrypt lock --verbose {self.path}") + self.mount_a.run_shell_payload(f'ls {snap_path}') + + def test_fscrypt_snap_mgr(self): + """ Test that mgr created snapshots are readable in unlocked state """ + v = "cephfs" + sv = "sv1" + ss = "ss1" + + self.run_ceph_cmd(f'fs subvolume create {v} {sv} --mode=777') + sv_path = self._get_sv_path(v, sv) + + # ensure subvol exists + self.mount_a.run_shell_payload(f'ls {sv_path}') + + # encrypt and unlock subvol + self.mount_a.run_shell_payload(f"sudo fscrypt encrypt --verbose --source=raw_key --name={self.protector} --no-recovery --key={self.key_file} {sv_path}") + + self.run_ceph_cmd(f'fs subvolume snapshot create {v} {sv} {ss}') + snap_path = f'{sv_path}/.snap/' + + #check snapshot name is same in unlocked/locked state + self.mount_a.run_shell_payload(f'sudo chmod 777 {sv_path}') + self.mount_a.run_shell_payload(f'ls {snap_path} | grep ^_{ss}') + self.mount_a.run_shell_payload(f"sudo fscrypt lock --verbose {sv_path}") + self.mount_a.run_shell_payload(f'ls {snap_path} | grep ^_{ss}') + + def test_fscrypt_clone(self): + """ Test that an fscrypt tree can be cloned """ + v = "cephfs" + sv = "sv1" + ss = "ss1" + c = "ss1c1" + + #generate tree + self.run_ceph_cmd(f'fs subvolume create {v} {sv} --mode=777') + src_path = self._get_sv_path(v, sv) + + self.mount_a.run_shell_payload(f"sudo fscrypt encrypt --verbose --source=raw_key --name={self.protector} --no-recovery --key={self.key_file} {src_path}") + self.mount_a.run_shell_payload(f'sudo chmod 777 {src_path}') + + num_of_files = 10 + dirs = 3 + for i in range(num_of_files): + self.mount_a.run_shell_payload(f'mkdir -p {src_path}/{i}') + for j in range(dirs): + rand_file = f'{src_path}/{i}/rand_file{j}' + block = ''.join(random.choice(string.ascii_letters) for _ in range(1 * 1024 * 1024)) + contents = block * 16 + with open(rand_file, 'w') as f: + f.write(contents) + + self.mount_a.symlink(rand_file, f'{rand_file}-sym') + + self.run_ceph_cmd(f'fs subvolume snapshot create {v} {sv} {ss}') + + self.run_ceph_cmd(f'fs subvolume snapshot clone {v} {sv} {ss} {c}') + self._wait_for_clone_to_complete(v, c) + + c_path = self._get_sv_path(v, c) + dst_path = f'{c_path}' + + #compare unlocked + self._compare_trees(src_path, dst_path) + + #compare locked + self.mount_a.run_shell_payload(f"sudo fscrypt lock --verbose {src_path}") + self._compare_trees(src_path, dst_path) + + def test_fscrypt_clone_long_name(self): + """ Test that an fscrypt tree with long names can be cloned """ + v = "cephfs" + sv = "sv1" + ss = "ss1" + c = "ss1c1" + + #generate tree + self.run_ceph_cmd(f'fs subvolume create {v} {sv} --mode=777') + src_path = self._get_sv_path(v, sv) + + self.mount_a.run_shell_payload(f'mkdir -p {src_path}') + + self.mount_a.run_shell_payload(f"sudo fscrypt encrypt --verbose --source=raw_key --name={self.protector} --no-recovery --key={self.key_file} {src_path}") + self.mount_a.run_shell_payload(f'sudo chmod 777 {src_path}') + + long_name = f'{src_path}/' + for i in range(255): + long_name += 'a' + + with open(long_name, 'w') as f: + f.write('contents') + + long_symlink = f'{src_path}/' + for i in range(255): + long_symlink += 's' + + self.mount_a.symlink(long_name, f'{long_symlink}') + + self.run_ceph_cmd(f'fs subvolume snapshot create {v} {sv} {ss}') + + self.run_ceph_cmd(f'fs subvolume snapshot clone {v} {sv} {ss} {c}') + self._wait_for_clone_to_complete(v, c) + + c_path = self._get_sv_path(v, c) + dst_path = f'{c_path}' + + #compare unlocked + self._compare_trees(src_path, dst_path) + + #compare locked + self.mount_a.run_shell_payload(f"sudo fscrypt lock --verbose {src_path}") + self._compare_trees(src_path, dst_path) + class TestFSCryptXFS(XFSTestsDev): def setup_xfsprogs_devs(self): -- 2.39.5