diff --git a/test-info.yml.example b/test-info.yml.example index e84c89c..17e97e8 100644 --- a/test-info.yml.example +++ b/test-info.yml.example @@ -1,20 +1,35 @@ -private_interfaces: - - "192.168.122.100" - - "192.168.122.101" +# Server hostname to be tested +server: server_name -public_interfaces: - - "192.168.123.10" - - "192.168.123.11" +# Users to use for authentication +users: + user1: user1password -exported_sharenames: - - "gluster-vol" +# Backend filesystem of the exported shares +backend: glusterfs -test_users: - - {"username": "test1", "password": "x"} - - {"username": "test2", "password": "x"} +# Additional information which may be used by tests here +# some tests may be skipped if the required config option is not set +extra: + # The supplementary group to be used by test_supplementary_group + supplementary_group: sg -test_backend: glusterfs - -premounted_shares: - - "/testdir1" - - "/testdir2" \ No newline at end of file +# shares: List of dict of exported shares +shares: + # share name export1 + export1: + # If present, it means the share is pre-mounted + path: /mnt/share/export1-cephfs-vfs + backend: + # If present backend filesystem + name: cephfs.vfs + # If present, provides direct access to share + path: /mnt/backend/export1 + # If present, username to perform the tests with on this share + users: + test2: x + # If present, the hostname to use to connect to the test server + server: hostname1 + # share name export2 + export2: + # Use default values set for this share diff --git a/testcases/consistency/test_consistency.py b/testcases/consistency/test_consistency.py index 555c835..aeb4982 100755 --- a/testcases/consistency/test_consistency.py +++ b/testcases/consistency/test_consistency.py @@ -61,9 +61,9 @@ def generate_consistency_check( if not test_info_file: return [] arr = [] - for ipaddr in test_info_file["public_interfaces"]: - for share_name in test_info_file["exported_sharenames"]: - arr.append((ipaddr, share_name)) + for share in testhelper.get_exported_shares(test_info): + s = testhelper.get_share(test_info, share) + arr.append((s["server"], s["name"])) return arr diff --git a/testcases/containers/test_containers.py b/testcases/containers/test_containers.py index 8569e19..ee1d423 100755 --- a/testcases/containers/test_containers.py +++ b/testcases/containers/test_containers.py @@ -66,12 +66,11 @@ def containers_check(ipaddr: str, share_name: str, test: str) -> None: def generate_containers_test() -> typing.List[typing.Tuple[str, str, str]]: - # Use the first given public_interface for our tests - ipaddr = test_info["public_interfaces"][0] arr = [] - for share_name in test_info["exported_sharenames"]: + for share_name in testhelper.get_exported_shares(test_info): + server = testhelper.get_share(test_info, share_name)["server"] for test in container_tests.keys(): - arr.append((ipaddr, share_name, test)) + arr.append((server, share_name, test)) return arr diff --git a/testcases/misc/conftest.py b/testcases/misc/conftest.py index 21e0531..c495fbc 100644 --- a/testcases/misc/conftest.py +++ b/testcases/misc/conftest.py @@ -46,12 +46,12 @@ def setup_mount( def gen_params() -> typing.List[typing.Any]: - ipaddr = test_info["public_interfaces"][0] - exported_sharenames = test_info.get("exported_sharenames", []) + exported_sharenames = testhelper.get_exported_shares(test_info) arr = [] for share_name in exported_sharenames: + server = testhelper.get_share(test_info, share_name)["server"] arr.append( - pytest.param((ipaddr, share_name), id=f"{ipaddr}-{share_name}") + pytest.param((server, share_name), id=f"{server}-{share_name}") ) return arr diff --git a/testcases/misc/test_supplemantary_group.py b/testcases/misc/test_supplemantary_group.py new file mode 100644 index 0000000..ce15363 --- /dev/null +++ b/testcases/misc/test_supplemantary_group.py @@ -0,0 +1,100 @@ +# This test is to check writes to a folder owned by a supplementary group. +# expected username in share["users"] and +# share["extra"]["supplementary_group"] with supplementary group +# also needs direct access to underlying filesystem +# +# Requirements: +# 1. username in share["user"] exists +# 2. username is part of supplementary group provided in +# user["extra"]["supplementary_group"] +# 3. share["backend"]["path"] should be set +# +# Steps: +# 1. Create folder test_subdir/ on direct path with +# group set to sgroup and mode 0770 +# 2. Upload file to test_subdir/test-cp +# +# Expected: +# Copy passes + +import testhelper +import pytest +import shutil +import pwd +import grp +import os +from pathlib import Path + +test_info_file = os.getenv("TEST_INFO_FILE") +test_info = testhelper.read_yaml(test_info_file) +test_string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +test_subdir = Path("supplementary_group") + + +def check_reqs(username: str, groupname: str) -> bool: + try: + pwd.getpwnam(username) + if username not in grp.getgrnam(groupname).gr_mem: + return False + except KeyError: + return False + return True + + +def setup_local_testdir(root: Path, group: str) -> Path: + testdir = root / test_subdir + testdir.mkdir(exist_ok=True) + shutil.chown(testdir, group=group) + testdir.chmod(0o770) + return testdir + + +def write_file_remote( + mount_point: Path, + ipaddr: str, + share_name: str, +) -> None: + mount_params = testhelper.get_mount_parameters(test_info, share_name) + mount_params["host"] = ipaddr + try: + test_file = testhelper.get_tmp_file(mount_point) + test_file_remote = test_subdir / Path("test-cp") + with open(test_file, "w") as f: + f.write(test_string) + put_cmds = "put %s %s" % (test_file, test_file_remote) + (ret, output) = testhelper.smbclient(mount_params, put_cmds) + assert ret == 0, "Failed to copy file to server: " + output + finally: + if test_file.exists(): + test_file.unlink() + + +def gen_supplementary_group_param(test_info: dict) -> list: + if not test_info: + return [] + sgroup = testhelper.get_conf_extra(test_info, "supplementary_group") + if sgroup is None: + return [] + arr = [] + for share in testhelper.get_shares_with_directmnt(test_info): + s = testhelper.get_share(test_info, share) + username = list(s["users"].keys())[0] + if check_reqs(username, sgroup): + arr.append((s["server"], share)) + return arr + + +@pytest.mark.parametrize( + "ipaddr,share_name", gen_supplementary_group_param(test_info) +) +def test_supplementary_group(ipaddr: str, share_name: str) -> None: + share = testhelper.get_share(test_info, share_name) + fs_path = Path(share["backend"]["path"]) + sgroup = testhelper.get_conf_extra(test_info, "supplementary_group") + testdir = setup_local_testdir(fs_path, sgroup) + try: + tmp_root = testhelper.get_tmp_root() + mount_point = testhelper.get_tmp_mount_point(tmp_root) + write_file_remote(mount_point, ipaddr, share_name) + finally: + shutil.rmtree(testdir) diff --git a/testcases/smbtorture/test_smbtorture.py b/testcases/smbtorture/test_smbtorture.py index e266c36..6c053b2 100755 --- a/testcases/smbtorture/test_smbtorture.py +++ b/testcases/smbtorture/test_smbtorture.py @@ -48,7 +48,8 @@ def smbtorture(share_name: str, test: str, tmp_output: Path) -> bool: "--expected-failures=" + script_root + "/selftest/" + filter ) flapping_list = ["flapping", "flapping.d"] - test_backend = test_info.get("test_backend") + share = testhelper.get_share(test_info, share_name) + test_backend = share["backend"].get("name") if test_backend is not None: flapping_file = "flapping." + test_backend flapping_file_path = os.path.join( @@ -114,7 +115,7 @@ def list_smbtorture_tests(): def generate_smbtorture_tests() -> typing.List[typing.Tuple[str, str]]: smbtorture_info = list_smbtorture_tests() arr = [] - for share_name in test_info.get("exported_sharenames", []): + for share_name in testhelper.get_exported_shares(test_info): for torture_test in smbtorture_info: arr.append((share_name, torture_test)) return arr diff --git a/testhelper/testhelper.py b/testhelper/testhelper.py index 47b51c6..0918e70 100644 --- a/testhelper/testhelper.py +++ b/testhelper/testhelper.py @@ -4,17 +4,59 @@ from pathlib import Path -def read_yaml(test_info): +def read_yaml(test_info_file): """Returns a dict containing the contents of the yaml file. Parameters: - test_info: filename of yaml file. + test_info_file: filename of yaml file. Returns: dict: The parsed test information yml as a dictionary. """ - with open(test_info) as f: + with open(test_info_file) as f: test_info = yaml.load(f, Loader=yaml.FullLoader) + + shares = test_info.get("shares", {}) + + # Copy exported_sharenames to shares + for sharename in test_info.get("exported_sharenames", []): + assert sharename not in shares, "Duplicate share name present" + shares[sharename] = {} + + # Add missing fields with defaults + # Todo : Remove old field names once sit-environment is updated + default_backend = test_info.get("backend") or test_info.get( + "test_backend", "xfs" + ) + default_server = ( + test_info.get("server") + or test_info.get("public_interfaces", ["localhost"])[0] + ) + default_users = test_info.get("users") + if default_users is None: + users = test_info.get("test_users", None) + if users is None: + default_users = {} + else: + for user in users: + default_users = {user["username"]: user["password"]} + + for share in shares: + if shares[share] is None: + shares[share] = {"name": share} + else: + shares[share]["name"] = share + if "backend" not in shares[share]: + shares[share]["backend"] = {} + if "name" not in shares[share]["backend"]: + shares[share]["backend"]["name"] = default_backend + if "server" not in share: + shares[share]["server"] = default_server + if "users" not in share: + shares[share]["users"] = default_users + + test_info["shares"] = shares + return test_info @@ -48,11 +90,14 @@ def get_mount_parameters(test_info: dict, share: str) -> typing.Dict[str, str]: test_info: Dict containing the parsed yaml file. share: The share for which to get the mount_params """ + s = get_share(test_info, share) + server = s["server"] + users = list(s["users"].keys()) return gen_mount_params( - test_info["public_interfaces"][0], + server, share, - test_info["test_users"][0]["username"], - test_info["test_users"][0]["password"], + users[0], + s["users"][users[0]], ) @@ -76,6 +121,46 @@ def generate_random_bytes(size: int) -> bytes: return rba[:size] +def get_shares(test_info: dict) -> dict: + """ + Get list of shares + + Parameters: + test_info: Dict containing the parsed yaml file. + Returns: + list of dict of shares + """ + return test_info["shares"] + + +def get_share(test_info: dict, sharename: str) -> dict: + """ + Get share dict for a given sharename + + Parameters: + test_info: Dict containing the parsed yaml file. + sharename: name of the share + Returns: + dict of the share + """ + shares = get_shares(test_info) + assert sharename in shares.keys(), "Share not found" + return shares[sharename] + + +def is_premounted_share(share: dict) -> bool: + """ + Check if the share is a premounted share + + Parameters: + share: dict of the share + Returns: + bool + """ + mntdir = share.get("path") + return mntdir is not None + + def get_premounted_shares(test_info: dict) -> typing.List[Path]: """ Get list of premounted shares @@ -85,5 +170,57 @@ def get_premounted_shares(test_info: dict) -> typing.List[Path]: Returns: list of paths with shares """ - premounted_shares = test_info.get("premounted_shares", []) - return [Path(mnt) for mnt in premounted_shares] + arr = [] + for share in get_shares(test_info).values(): + if is_premounted_share(share): + arr.append(Path(share["path"])) + return arr + + +def get_exported_shares(test_info: dict) -> typing.List[str]: + """Get the list of exported shares + + Parameters: + test_info: Dict containing the parsed yaml file. + Returns: + list of exported shares + """ + arr = [] + for share in get_shares(test_info).values(): + if not is_premounted_share(share): + arr.append(share["name"]) + return arr + + +def get_shares_with_directmnt(test_info: dict) -> typing.List[str]: + """ + Get list of shares with directmnt enabled + + Parameters: + test_info: Dict containing the parsed yaml file. + Returns: + list of shares + """ + arr = [] + for share in get_shares(test_info).values(): + if share["backend"].get("path", False): + arr.append(share["name"]) + return arr + + +def get_conf_extra(test_info: dict, key: str) -> typing.Any: + """ + We can pass a test specific configuration under extra in the + test-info.yml file. This function allows easy access to this + configuration section. + + Parameters: + test_info: Dict containing the parsed yaml file. + key: key for the extra configuration + Returns: + contents in extra[key] + """ + extra = test_info.get("extra") + if extra is None or key not in extra: + return None + return extra[key]