diff --git a/src/tests/ftest/pool/list_pools.py b/src/tests/ftest/pool/list_pools.py index b1a85a0125d7..c988d72c9153 100644 --- a/src/tests/ftest/pool/list_pools.py +++ b/src/tests/ftest/pool/list_pools.py @@ -1,5 +1,5 @@ """ - (C) Copyright 2018-2023 Intel Corporation. + (C) Copyright 2018-2024 Intel Corporation. SPDX-License-Identifier: BSD-2-Clause-Patent """ @@ -126,3 +126,151 @@ def test_list_pools(self): test_case, error)) if errors: self.fail("Errors detected:\n {}".format("\n ".join(errors))) + + +class ListClientPoolsTest(TestWithServers): + """Test class for daos pool list tests. + + Test Class Description: + This class contains tests for client-side pool list. + + :avocado: recursive + """ + + def run_case(self, rank_lists, svcn=None): + """Run test case. + + Create pools, call daos pool list to get the list, and compare against + the UUIDs and service replicas returned at create time. The first pool + created will be marked inaccessible to clients, and should not show up + in the client list. + + Args: + rank_lists (list): Rank lists. List of list of int. + svcn (str, optional): Service replicas. Defaults to None. + + Raises: + CommandFailure: if there was an error destroying pools + TestFail: if there was an error verifying the created pools + + """ + # Iterate rank lists to create pools. Store the created pool information + # as a dictionary of pool UUID keys with a service replica list value. + self.pool = [] + expected_admin_uuids = {} + for rank_list in rank_lists: + self.pool.append(self.get_pool(target_list=rank_list, svcn=svcn)) + expected_admin_uuids[self.pool[-1].uuid.lower()] = self.pool[-1].svc_ranks + + # Remove the default ACLs that allow the creator to access the pool. + # These ACLs don't apply to dmg, but do apply to users. + offlimits = self.pool[0] + self.get_dmg_command().pool_delete_acl(offlimits.uuid, 'OWNER@') + self.get_dmg_command().pool_delete_acl(offlimits.uuid, 'GROUP@') + expected_user_uuids = expected_admin_uuids.copy() + del expected_user_uuids[offlimits.uuid.lower()] + + # Verify the 'dmg pool list' command lists the correct created pool + # information. + detected_admin_uuids = {} + try: + for data in self.get_dmg_command().get_pool_list_all(): + detected_admin_uuids[data["uuid"]] = data["svc_reps"] + except KeyError as error: + self.fail("Error parsing dmg pool list output: {}".format(error)) + + self.log.info("Expected admin pool info: %s", str(expected_admin_uuids)) + self.log.info("Detected admin pool info: %s", str(detected_admin_uuids)) + + # Compare the expected and detected pool information + self.assertEqual( + expected_admin_uuids, detected_admin_uuids, + "dmg pool info does not list all expected pool UUIDs and their " + "service replicas") + + # Verify the 'daos pool list' command lists the correct created pool + # information. + detected_user_uuids = {} + try: + for data in self.get_daos_command().get_pool_list_all(): + detected_user_uuids[data["uuid"]] = data["svc_reps"] + except KeyError as error: + self.fail("Error parsing dmg pool list output: {}".format(error)) + + self.log.info("Expected user pool info: %s", str(expected_user_uuids)) + self.log.info("Detected user pool info: %s", str(detected_user_uuids)) + + # Compare the expected and detected pool information + self.assertEqual( + expected_user_uuids, detected_user_uuids, + "daos pool info does not list all expected pool UUIDs and their " + "service replicas") + + # Destroy all the pools + if self.destroy_pools(self.pool): + self.fail("Error destroying pools") + self.pool = [] + + + def test_list_client_pools(self): + """JIRA ID: DAOS-16038. + + Test Description: + Create pools in different ranks, call daos pool list and verify the + output list matches the output returned when the pools were + created. The first pool created should be inaccessible to the client, + and should not be returned in the list. + + :avocado: tags=all,full_regression + :avocado: tags=vm + :avocado: tags=pool + :avocado: tags=ListClientPoolsTest,test_list_client_pools + """ + ranks = list(range(len(self.hostlist_servers))) + + # Create pools with different ranks: + # 1) Create 4 pools each using a different rank number + # 2) Create 2 pools using 2 ranks per pool + # 3) Create 4 pools each using the same 4 ranks for each pool + # 4) Create 3 pools using all ranks with --svcn=3 + test_cases = [ + ( + "Create 4 pools each using a different rank number", + {"rank_lists": [[rank] for rank in ranks[:4]]} + ), + ( + "Create 2 pools using 2 ranks per pool", + { + "rank_lists": + [ranks[index:index + 2] + for index in range(0, len(ranks[:4]), 2)] + } + ), + ( + "Create 4 pools each using the same 4 ranks for each pool", + {"rank_lists": [ranks[:4] for _ in ranks[:4]]} + ), + ( + "Create 3 pools using all ranks with --nsvc=3", + {"rank_lists": [None for _ in ranks[:3]], "svcn": 3} + ), + ] + errors = [] + for test_case, kwargs in test_cases: + self.log.info("%s", "-" * 80) + self.log.info("Running test case: %s", test_case) + self.log.info("%s", "-" * 80) + try: + self.run_case(**kwargs) + + except TestFail as error: + message = "Error: {}: {}".format(test_case, error) + self.log.info(message) + errors.append(message) + + except CommandFailure as error: + self.fail( + "Fatal test error detected during: {}: {}".format( + test_case, error)) + if errors: + self.fail("Errors detected:\n {}".format("\n ".join(errors))) diff --git a/src/tests/ftest/util/daos_utils.py b/src/tests/ftest/util/daos_utils.py index 679ca4324143..13dfdbdd9f33 100644 --- a/src/tests/ftest/util/daos_utils.py +++ b/src/tests/ftest/util/daos_utils.py @@ -369,6 +369,113 @@ def pool_list_attrs(self, pool, sys_name=None, verbose=False): ("pool", "list-attrs"), pool=pool, sys_name=sys_name, verbose=verbose) + def pool_list_containers(self, pool, sys_name=None): + """List containers in the pool. + + Args: + pool (str): pool label or UUID + sys_name (str): DAOS system name. Defaults to None. + + Returns: + dict: JSON output + + Raises: + CommandFailure: if the daos pool list-containers command fails. + + """ + return self._get_json_result( + ("pool", "list-containers"), pool=pool, sys_name=sys_name) + + def pool_list(self, no_query=False, verbose=False): + """List pools. + + Args: + no_query (bool, optional): If True, do not query for pool stats. + verbose (bool, optional): If True, use verbose mode. + + Raises: + CommandFailure: if the daos pool list command fails. + + Returns: + dict: the daos json command output converted to a python dictionary + + """ + # Sample verbose JSON Output: + # { + # "response": { + # "status": 0, + # "pools": [ + # { + # "uuid": "517217db-47c4-4bb9-aae5-e38ca7b3dafc", + # "label": "mkp1", + # "svc_reps": [ + # 0 + # ], + # "total_targets": 8, + # "disabled_targets": 0, + # "usage": [ + # { + # "tier_name": "SCM", + # "size": 3000000000, + # "free": 2995801112, + # "imbalance": 0 + # }, + # { + # "tier_name": "NVME", + # "size": 47000000000, + # "free": 26263322624, + # "imbalance": 36 + # } + # ] + # } + # ] + # }, + # "error": null, + # "status": 0 + # } + return self._get_json_result( + ("pool", "list"), no_query=no_query, verbose=verbose) + + def _parse_pool_list(self, key=None, **kwargs): + """Parse the daos pool list json output for the requested information. + + Args: + key (str, optional): pool list json dictionary key in + ["response"]["pools"]. Defaults to None. + + Raises: + CommandFailure: if the daos pool list command fails. + + Returns: + list: list of all the pool items in the daos pool list json output + for the requested json dictionary key. This will be an empty + list if the key does not exist or the json output was not in + the expected format. + + """ + pool_list = self.pool_list(**kwargs) + try: + if pool_list["response"]["pools"] is None: + return [] + if key: + return [pool[key] for pool in pool_list["response"]["pools"]] + return pool_list["response"]["pools"] + except KeyError: + return [] + + def get_pool_list_all(self, **kwargs): + """Get a list of all the pool information from daos pool list. + + Raises: + CommandFailure: if the daos pool list command fails. + + Returns: + list: a list of dictionaries containing information for each pool + from the daos pool list json output + + """ + return self._parse_pool_list(**kwargs) + def container_query(self, pool, cont, sys_name=None): """Query a container. diff --git a/src/tests/ftest/util/daos_utils_base.py b/src/tests/ftest/util/daos_utils_base.py index 9130b214dac9..cfb77ebd2d68 100644 --- a/src/tests/ftest/util/daos_utils_base.py +++ b/src/tests/ftest/util/daos_utils_base.py @@ -47,8 +47,10 @@ def __init__(self): def get_sub_command_class(self): # pylint: disable=redefined-variable-type - """Get the dmg network sub command object.""" - if self.sub_command.value in ("list-containers", "list"): + """Get the daos pool sub command object.""" + if self.sub_command.value == "list": + self.sub_command_class = self.ListSubCommand() + if self.sub_command.value == "list-containers": self.sub_command_class = self.ListContainersSubCommand() elif self.sub_command.value == "query": self.sub_command_class = self.QuerySubCommand() @@ -85,6 +87,13 @@ def __init__(self, sub_command): self.sys_name = FormattedParameter("--sys-name={}") self.sys = FormattedParameter("--sys={}") + class ListSubCommand(CommonPoolSubCommand): + """Defines an object for the daos pool list command.""" + + def __init__(self): + """Create a daos pool list command object.""" + super().__init__("list") + class ListContainersSubCommand(CommonPoolSubCommand): """Defines an object for the daos pool list-containers command."""