示例#1
0
class CRUDOperations(CtsTestScript):
    TIMEOUT = 600
    DESCRIPTION = """test script validating CRUD (create, read, update and delete) functionalities"""

    def run(self):
        if self.metadata_container is None:
            return

        self.chosen_endpoint, self.chosen_zone, self.chosen_volume, self.chosen_volume_collection = (None,)*4
        self.initiator_endpoint, self.target_endpoint, self.zone_endpoint, self.created_volume = (None,)*4

        test_name = "Storage Services CRUD test"
        print "MESSAGE::%s starting" % test_name

        print "TEST_CASE::API crawling"
        api_explorer = ApiExplorer(self.metadata_container, self.configuration)
        self.discovery_container, status = api_explorer.discover(MetadataConstants.SERVICE_ROOT_URI,
                                                                 MetadataConstants.SERVICE_ROOT)
        print "STATUS::{status}".format(status=status)

        requirements = [
            Requirement(MetadataConstants.SERVICE_ROOT, min=1, max=1),
            Requirement(MetadataConstants.STORAGE_SERVICE, min=1),
            Requirement(MetadataConstants.VOLUME, min=1),
            Requirement(MetadataConstants.VOLUME_COLLECTION, min=1)
        ]
        preconditions = Preconditions(self.metadata_container, requirements)
        status = preconditions.validate(self.discovery_container)

        if status == ValidationStatus.FAILED:
            self.set_status_failed()
            return

        self.api_caller = ApiCaller(self.configuration)

        status = ValidationStatus.join_statuses(status, self.crud_fabrics_target())

        print "MESSAGE::%s overall status: %s" % (test_name, ColorPrinter.format_status(status))

    def crud_fabrics_target(self):
        status = ValidationStatus.PASSED

        cts_message("CRUD FABRICS TARGET")

        # test pre check
        self.__find_volumes_without_any_links(optional_pre_check=True)

        try:
            status = ValidationStatus.join_statuses(status, self._test_create_volume())
        except:
            self._test_delete_all_created_endpoints_and_zones()
            status = ValidationStatus.FAILED

        self.__find_and_choose_endpoint_collection()
        self.__find_and_choose_volume()

        if not (self.chosen_volume and self.chosen_endpoint and self.chosen_zone):
            self.set_status_blocked()
            return ValidationStatus.BLOCKED

        cts_message("Chosen Volume for test: {volume_url}".format(volume_url=self.chosen_volume))

        try:
            status = ValidationStatus.join_statuses(status, self._test_create_initiator_endpoint())
            status = ValidationStatus.join_statuses(status, self._test_create_target_endpoint())
            status = ValidationStatus.join_statuses(status, self._test_create_zone_with_only_one_endpoint())
            status = ValidationStatus.join_statuses(status, self._test_patch_zone_with_target_endpoint())
        except:
            status = ValidationStatus.FAILED
        finally:
            status = ValidationStatus.join_statuses(status, self._test_delete_all_created_endpoints_and_zones())

        return status

    # Test case 1:
    def _test_create_volume(self):
        print "TEST_CASE::Create volume"

        post_volume_payload = dict(
            CapacityBytes=10000000,
            CapacitySources=[],
            AccessCapabilities=[]
        )

        status, status_code, response_body, headers = self.api_caller.post_resource(
            self.chosen_volume_collection,
            self.discovery_container,
            payload=post_volume_payload,
            acceptable_return_codes=[ReturnCodes.OK, ReturnCodes.ACCEPTED, ReturnCodes.CREATED]
        )

        if not status:
            cts_error("CTS cannot create volume")
            self.set_status_failed()
            return ValidationStatus.FAILED

        self.created_volume = headers['location']
        cts_message("Volume created at {location}".format(location=self.created_volume))

        if not self._verify_size_of_created_volume(self.created_volume, post_volume_payload):
            cts_error("Service create volume with wrong capacity")
            self.set_status_failed()
            return ValidationStatus.FAILED

        self.set_status_passed()
        return ValidationStatus.PASSED

    # Test case 2:
    def _test_create_initiator_endpoint(self):
        print "TEST_CASE::Create initiator endpoint"

        random_suffix = random.randrange(1, RANDOM_RANGE, 1)
        post_initiator_endpoint_payload = dict(
            Identifiers=[dict(
                DurableName="iqn.initiator" + str(random_suffix),
                DurableNameFormat="iQN"
              )],
            ConnectedEntities=[dict(
                EntityRole="Initiator",
            )],
            IPTransportDetails=[],
            Oem=dict(
                Intel_RackScale=dict(
                    Authentication=dict(
                        Username="******" + str(random_suffix),
                        Password="******"
                    ))))

        status, status_code, response_body, headers = self.api_caller.post_resource(
            self.chosen_endpoint,
            self.discovery_container,
            payload=post_initiator_endpoint_payload,
            acceptable_return_codes=[ReturnCodes.OK, ReturnCodes.ACCEPTED, ReturnCodes.CREATED]
        )

        if not status:
            cts_error("CTS cannot create Initiator Endpoint")
            self.set_status_failed()
            return ValidationStatus.FAILED

        self.initiator_endpoint = headers['location']
        cts_message("Initiator Endpoint created at {location}".format(location=self.initiator_endpoint))

        try:
            verify = self.__verify_created_initiator_endpoint(self.initiator_endpoint, post_initiator_endpoint_payload)
        except:
            verify = False

        if not verify:
            cts_error("Service create initiator endpoint with wrong value")
            self.set_status_failed()
            return ValidationStatus.FAILED

        self.set_status_passed()
        return ValidationStatus.PASSED

    # Test case 3:
    def _test_create_target_endpoint(self):
        print "TEST_CASE::Create target endpoint"

        selected_volume = self.discovery_container.get(self.chosen_volume)
        random_suffix = random.randrange(1, RANDOM_RANGE, 1)
        post_target_endpoint_payload = dict(
            Identifiers=[dict(
                DurableName="iqn.initiator" + str(random_suffix),
                DurableNameFormat="iQN"
            )],
            ConnectedEntities=[dict(
                EntityRole="Target",
                EntityLink={"@odata.id": selected_volume.odata_id},
            )],
            IPTransportDetails=[],
            Oem=dict(
                Intel_RackScale=dict(
                    Authentication=dict(
                        Username="******" + str(random_suffix),
                        Password="******"
                    ))))

        status, status_code, response, headers = self.api_caller.post_resource(
                                                                          self.chosen_endpoint,
                                                                          self.discovery_container,
                                                                          payload=post_target_endpoint_payload,
                                                                          acceptable_return_codes=[ReturnCodes.OK,
                                                                                                   ReturnCodes.ACCEPTED,
                                                                                                   ReturnCodes.CREATED])

        if not status:
            cts_error("CTS can not create Target Endpoint")
            self.set_status_failed()
            return ValidationStatus.FAILED

        self.target_endpoint = headers['location']
        cts_message("Target Endpoint created at {location}".format(location=self.target_endpoint))

        try:
            verify = self.__verify_created_target_endpoint(self.target_endpoint, post_target_endpoint_payload)
        except:
            verify = False

        if not verify:
            cts_error("Service create target endpoint with wrong value")
            self.set_status_failed()
            return ValidationStatus.FAILED

        self.set_status_passed()
        return ValidationStatus.PASSED

    # Test case 4:
    def _test_create_zone_with_only_one_endpoint(self):
        print "TEST_CASE::Create zone with only one endpoint"

        try:
            initiator_endpoint_link = self.__get_odata_id_links(self.initiator_endpoint)
        except:
            cts_error("No valid initiator endpoint for zone creation")
            self.set_status_blocked()
            return ValidationStatus.BLOCKED


        post_create_zone_payload = dict(
            Links=dict(
                Endpoints=[{"@odata.id": initiator_endpoint_link}]
            )
        )

        status, status_code, _, headers = self.api_caller.post_resource(self.chosen_zone,
                                                                        self.discovery_container,
                                                                        payload=post_create_zone_payload,
                                                                        acceptable_return_codes=[ReturnCodes.ACCEPTED,
                                                                                                 ReturnCodes.CREATED,
                                                                                                 ReturnCodes.OK])

        if not status:
            cts_error("CTS can not create Zone")
            self.set_status_failed()
            return ValidationStatus.FAILED

        self.zone_endpoint = headers['location']
        cts_message("Zone created at {location}".format(location=self.zone_endpoint))

        if not self.__verify_created_zone(self.zone_endpoint, post_create_zone_payload):
            cts_error("Service zone created with wrong value")
            self.set_status_failed()
            return ValidationStatus.FAILED

        self.set_status_passed()
        return ValidationStatus.PASSED

    # Test case 5:
    def _test_patch_zone_with_target_endpoint(self):
        print "TEST_CASE::Patch zone with target endpoint"

        initiator_or_target_missing = False

        try:
            initiator_endpoint_link = self.__get_odata_id_links(self.initiator_endpoint)
        except:
            cts_error("No valid initiator endpoint for zone patching")
            initiator_or_target_missing = True

        try:
            target_endpoint_link = self.__get_odata_id_links(self.target_endpoint)
        except:
            cts_error("No valid target endpoint for zone patching")
            initiator_or_target_missing = True

        if initiator_or_target_missing:
            cts_error("Missing resources for zone patching")
            self.set_status_blocked()

            return ValidationStatus.BLOCKED

        patch_create_zone_payload = dict(
            Links=dict(
                Endpoints=[
                    {"@odata.id": initiator_endpoint_link},
                    {"@odata.id": target_endpoint_link}]
        ))

        status, status_code, _, headers = self.api_caller.patch_resource(self.zone_endpoint,
                                                                         self.discovery_container,
                                                                         payload=patch_create_zone_payload,
                                                                         acceptable_return_codes=[ReturnCodes.ACCEPTED,
                                                                                                  ReturnCodes.OK])
        if not status:
            cts_error("CTS can not PATCH Zone")
            self.set_status_failed()
            return ValidationStatus.FAILED

        cts_message("Zone PATCHED at {location}".format(location=self.zone_endpoint))

        if not self.__verify_patched_zone(self.zone_endpoint, patch_create_zone_payload):
            cts_error("Service zone patched with wrong value")
            self.set_status_failed()
            return ValidationStatus.FAILED

        self.set_status_passed()
        return ValidationStatus.PASSED

    # Test case 6:
    def _test_delete_all_created_endpoints_and_zones(self):
        status = ValidationStatus.PASSED
        for resource, link in OrderedDict([("volume", self.created_volume),
                                           ("zone", self.zone_endpoint),
                                           ("initiator endpoint", self.initiator_endpoint),
                                           ("target endpoint", self.target_endpoint)]).iteritems():
            print "TEST_CASE::Delete {resource}".format(resource=resource)
            if link:
                if not self.__delete_location(link):
                    self.set_status_failed()
                    status = ValidationStatus.join_statuses(status, ValidationStatus.FAILED)
                else:
                    self.set_status_passed()
                    status = ValidationStatus.join_statuses(status, ValidationStatus.PASSED)
            else:
                self.set_status_blocked()
                status = ValidationStatus.join_statuses(status, ValidationStatus.BLOCKED)
        return status

    def __check_resources_have_same_parent_url(self, res1, res2):
        return (self.discovery_container.get(res1)).parent_url == (self.discovery_container.get(res2)).parent_url

    def __choose_endpoint_and_zone_from_fabric_collection(self, endpoint_collection, zone_collection):
        for ec in endpoint_collection:
            for zc in zone_collection:
                if self.__check_resources_have_same_parent_url(ec, zc):
                    return ec, zc
        return None, None

    def __find_and_choose_volume(self):
        for volume in self.__find_volumes_without_any_links():
            self.chosen_volume = volume
            break

    def __delete_location(self, location_url):
        status, _, _, _ = self.api_caller.delete_resource(location_url,
                                                          self.discovery_container,
                                                          acceptable_return_codes=[ReturnCodes.NO_CONTENT])
        if not status:
            cts_error("Resource at %s was not properly deleted" % location_url)
            return False
        return self.__verify_resource_non_exist(location_url)

    def __verify_resource_non_exist(self, location_url_verification):
        status, _, _, _, _ = self.api_caller.get_resource(location_url_verification,
                                                          self.discovery_container,
                                                          acceptable_return_codes=[ReturnCodes.BAD_REQUEST,
                                                                                   ReturnCodes.NOT_FOUND])
        if not status:
            cts_error("Resource at %s was not properly deleted" % location_url_verification)
            return False
        cts_message("Resource at %s was properly deleted" % location_url_verification)
        return True

    def __find_and_choose_endpoint_collection(self):
        endpoint_collection = dict(self.discovery_container.get_resources(
                                    MetadataConstants.ENDPOINT_COLLECTION))

        zones_collection = dict(self.discovery_container.get_resources(
                                    MetadataConstants.ZONE_COLLECTION))

        if not endpoint_collection:
            cts_error("No Endpoint Collection")
            self.set_status_blocked()

        if not zones_collection:
            cts_error("No Zone Collection")
            self.set_status_blocked()

        self.chosen_endpoint, self.chosen_zone = \
            self.__choose_endpoint_and_zone_from_fabric_collection(
                    endpoint_collection,
                    zones_collection)

    def __find_volumes_without_any_links(self, optional_pre_check=False):
        reusable_volumes = []
        logical_drive_collections = dict(self.discovery_container.get_resources(
                                         MetadataConstants.VOLUME_COLLECTION,
                                         any_child_version=True))

        # Allocated Volumes are connected with this same type as Volume
        volumes_collection_wo_allocated_volumes = [x for x in logical_drive_collections if not "AllocatedVolumes" in x]

        for collection in volumes_collection_wo_allocated_volumes:
            if not self.chosen_volume_collection:
                self.chosen_volume_collection = collection
            logical_drive_groups = dict(self.discovery_container.get_resources(MetadataConstants.VOLUME,
                                                                               any_child_version=True))
            volumes_collection = []

            for c in logical_drive_groups.keys():
                try:
                    # this Volume we can use to our tests
                    if len(self.discovery_container.get(c).body["Links"]["Oem"]["Intel_RackScale"]["Endpoints"]) == 0:
                        volumes_collection.append(c)
                except KeyError:
                    pass

            # if there are any free Volume, suspend test
            if len(volumes_collection) == 0:
                if optional_pre_check:
                    return False
                cts_error("No reusable Volume. Create a Volume without linked Endpoints")
                self.set_status_blocked()
            else:
                reusable_volumes.extend(volumes_collection)
        return reusable_volumes

    def __get_odata_id_links(self, link):
        return (self.discovery_container.get(link)).odata_id

    # verification code
    def __verify_patched_zone(self, resource_location, resource_payload):
        print "MESSAGE::Comparing newly patched zone to given payload"
        return self.__verify_zone(resource_location, resource_payload)

    def __verify_created_zone(self, resource_location, resource_payload):
        print "MESSAGE::Comparing newly created zone to given payload"
        return self.__verify_zone(resource_location, resource_payload)

    def __verify_zone(self, resource_location, resource_payload):
        location_payload_to_compare = JsonComparator.odict_to_dict(
                                            self.discovery_container.get(resource_location).body["Links"]["Endpoints"])
        payload_to_compare = resource_payload["Links"]["Endpoints"]

        return JsonComparator.compare_lists(location_payload_to_compare, payload_to_compare)

    def __verify_created_target_endpoint(self, resource_location, resource_payload):
        print "MESSAGE::Comparing newly created target endpoint to given payload"

        location_payload_to_compare = JsonComparator.odict_to_dict(
                                            self.discovery_container.get(resource_location).body)
        payload_to_compare = resource_payload

        verification_dict = {'ConnectedEntities': ['EntityLink',
                                                   'EntityRole'],
                             'Identifiers': ['DurableNameFormat',
                                             'DurableName']}

        return self.__verify_body(location_payload_to_compare, payload_to_compare, verification_dict)

    def __verify_created_initiator_endpoint(self, resource_location, resource_payload):
        print "MESSAGE::Comparing newly created initiator endpoint to given payload"
        location_payload_to_compare = JsonComparator.odict_to_dict(
                                            self.discovery_container.get(resource_location).body)
        payload_to_compare = resource_payload

        verification_dict = {'ConnectedEntities': ['EntityRole'],
                             'Identifiers': ['DurableNameFormat',
                                             'DurableName']}

        return self.__verify_body(location_payload_to_compare, payload_to_compare, verification_dict)

    def _verify_size_of_created_volume(self, resource_location, resource_payload):
        print "MESSAGE::Comparing newly created volume to given payload"

        discovery_service_capacity = self.discovery_container.get(resource_location).body["CapacityBytes"]
        resource_payload_capacity = resource_payload["CapacityBytes"]

        return self.__verify_bytes(resource_payload_capacity, discovery_service_capacity)

    @staticmethod
    def __verify_body(resource_1, resource_2, verification_dict):
        try:
            for verification_key in verification_dict.keys():
                for verification_elem in verification_dict[verification_key]:
                    if resource_1[verification_key][0][verification_elem] != \
                            resource_2[verification_key][0][verification_elem]:
                        return False
        except KeyError:
            return False
        return True

    @staticmethod
    def __verify_bytes(resource_on_payload, resource_discovered, min_bytes=None, max_bytes=None):
        if not min_bytes:
            # default 4 mb
            min_bytes = 4000000
        if not max_bytes:
            # default 4 mb
            max_bytes = 4000000

        if (resource_on_payload - min_bytes) <= resource_discovered <= (resource_on_payload + max_bytes):
            return True
        return False

    @staticmethod
    def __verify_strings(resource_on_payload, resource_discovered):
        resource_on_payload = str(resource_on_payload).lower()
        resource_discovered = str(resource_discovered).lower()

        return resource_on_payload == resource_discovered
示例#2
0
class ApiCallerCallsUnitTest(unittest.TestCase):
    def setUp(self):
        configuration = Configuration(**dict(ApiEndpoint=API_ENDPOINT))
        self.api_caller = ApiCaller(configuration)

    def test_get_on_empty_resource(self):
        with mock.patch('requests.get') as requests_get_mock:
            response = Mock()
            response.status_code = 200
            response.headers = {}
            response.text = ""
            requests_get_mock.return_value = response
            link, status, status_code, response_body, headers = \
                self.api_caller.get_resource("/resource", DiscoveryContainer())
            self.assertFalse(status)

    def test_incorrect_status_code(self):
        with mock.patch('requests.get') as requests_get_mock:
            response = Mock()
            response.status_code = 500
            response.headers = {}
            response.text = "{}"
            requests_get_mock.return_value = response
            link, status, status_code, response_body, headers = \
                self.api_caller.get_resource("/resource", DiscoveryContainer())
            self.assertFalse(status)

    def test_request_exception(self):
        with mock.patch('requests.get') as requests_get_mock:
            requests_get_mock.side_effect = requests.RequestException()
            with StdoutCapture() as output:
                self.api_caller.get_resource("/resource", DiscoveryContainer())
            self.assertIn("ERROR::Get url=http://1.2.3.4:567/resource Error <class 'requests.exceptions.RequestException'>:;", output.raw)

    def test_connection_error_exception(self):
        with mock.patch('requests.get') as requests_get_mock:
            requests_get_mock.side_effect = requests.ConnectionError()
            with StdoutCapture() as output:
                self.api_caller.get_resource("/resource", DiscoveryContainer())
            self.assertIn("ERROR::Get url=http://1.2.3.4:567/resource Error <class 'requests.exceptions.ConnectionError'>:;", output.raw)

    def test_http_error_exception(self):
        with mock.patch('requests.get') as requests_get_mock:
            requests_get_mock.side_effect = requests.HTTPError()
            with StdoutCapture() as output:
                self.api_caller.get_resource("/resource", DiscoveryContainer())
            self.assertIn("ERROR::Get url=http://1.2.3.4:567/resource Error <class 'requests.exceptions.HTTPError'>:", output.raw)

    def test_url_required_exception(self):
        with mock.patch('requests.get') as requests_get_mock:
            requests_get_mock.side_effect = requests.URLRequired()
            with StdoutCapture() as output:
                self.api_caller.get_resource("/resource", DiscoveryContainer())
            self.assertIn("ERROR::Get url=http://1.2.3.4:567/resource Error <class "
                          "'requests.exceptions.URLRequired'>:",
                          output.raw)

    def test_to_many_redirects_exception(self):
        with mock.patch('requests.get') as requests_get_mock:
            requests_get_mock.side_effect = requests.TooManyRedirects()
            with StdoutCapture() as output:
                self.api_caller.get_resource("/resource", DiscoveryContainer())
            self.assertIn("ERROR::Get url=http://1.2.3.4:567/resource Error <class 'requests.exceptions.TooManyRedirects'>:;", output.raw)

    def test_timeout_exception(self):
        with mock.patch('requests.get') as requests_get_mock:
            requests_get_mock.side_effect = requests.Timeout()
            with StdoutCapture() as output:
                self.api_caller.get_resource("/resource", DiscoveryContainer())
            self.assertIn("ERROR::Get url=http://1.2.3.4:567/resource Error <class 'requests.exceptions.Timeout'>:;", output.raw)

    def test_incorrect_body(self):
        with mock.patch('requests.get') as requests_get_mock:
            response = Mock()
            response.status_code = 200
            response.headers = {}
            response.text = "not a json"
            requests_get_mock.return_value = response

            with StdoutCapture() as output:
                self.api_caller.get_resource("/resource", DiscoveryContainer())
            self.assertIn(
                "ERROR", output.raw)

    def test_no_content(self):
        with mock.patch('requests.get') as requests_get_mock:
            response = Mock()
            response.status_code = 204
            response.headers = {}
            response.text = None
            requests_get_mock.return_value = response

            link, status, status_code, response_body, headers = \
                self.api_caller.get_resource("/resource", DiscoveryContainer(), acceptable_return_codes = [204])
            self.assertTrue(status)
            self.assertEqual(status_code, 204)
            self.assertEqual(response_body, dict())
            response.json.assert_not_called()

    def test_async_create_without_location(self):
        with mock.patch('requests.post') as requests_post_mock:
            response = Mock()
            response.status_code = ReturnCodes.ACCEPTED
            response.headers = {}
            response.text = None
            requests_post_mock.return_value = response

            with mock.patch('requests.get') as requests_get_mock:
                response = Mock()
                response.status_code = 204
                response.headers = {}
                response.text = None
                requests_get_mock.return_value = response

                with StdoutCapture() as output:
                    status, status_code, response_body, headers = self.api_caller.post_resource('odata_id', DiscoveryContainer(), payload={})

            self.assertIn('Location header not found', '\n'.join(output))

    def test_unexpected_async_post_response(self):
        with mock.patch('requests.post') as requests_get_mock:
            response = Mock()
            response.status_code = ReturnCodes.ACCEPTED
            response.headers = {}
            response.text = "{}"
            requests_get_mock.return_value = response

            with self.assertRaises(AsyncOperation):
                self.api_caller.post_resource('odata_id', DiscoveryContainer(), payload={}, wait_if_async=False)

    def test_async_post_response(self):
        with mock.patch('requests.post') as requests_post_mock:
            not_done = Mock()
            not_done.status_code = ReturnCodes.ACCEPTED
            not_done.headers = {'Location' : 'location'}
            not_done.text = None
            requests_post_mock.return_value = not_done

            with mock.patch('requests.get') as requests_get_mock:
                done = Mock()
                done.status_code = ReturnCodes.OK
                done.headers = {'Location' : 'location'}
                done.text = "{ \"done\": true }"

                requests_get_mock.side_effect = [not_done, not_done, done]

                with StdoutCapture() as output:
                    status, status_code, response_body, headers = self.api_caller.post_resource('odata_id',
                                                                                                DiscoveryContainer(),
                                                                                                payload={})
                    self.assertTrue(response_body['done'])

    def test_resource_in_discovery_container_after_get_patch_delete(self):
        with mock.patch('requests.get') as requests_get_mock:
            resource = {"@odata.id": "odata.id", "something": "irrelevant"}
            get_response = Mock()
            get_response.status_code = ReturnCodes.OK
            get_response.headers = {}
            get_response.text = json.dumps(resource)
            requests_get_mock.return_value = get_response
            discovery_container = DiscoveryContainer()
            self.api_caller.get_resource("/resource", discovery_container)
            self.assertEqual(discovery_container["http://{API_ENDPOINT}/resource".format(
                API_ENDPOINT=API_ENDPOINT)].body,
                             resource)

            patched_resource = {"@odata.id": "odata.id", "something": "relevant"}
            get_response.text = json.dumps(patched_resource)

            with mock.patch('requests.patch') as requests_patch_mock:
                patch_response = Mock()
                patch_response.status_code = ReturnCodes.OK
                patch_response.headers = {}
                patch_response.text = "{}"
                requests_patch_mock.return_value = patch_response
                _, _, _, _ = self.api_caller.patch_resource("/resource", discovery_container)
                self.assertEqual(discovery_container["http://{API_ENDPOINT}/resource".format(
                API_ENDPOINT=API_ENDPOINT)].body, patched_resource)

                with mock.patch('requests.delete') as requests_delete_mock:
                    delete_response = Mock()
                    delete_response.status_code = ReturnCodes.NO_CONTENT
                    delete_response.headers = {}
                    delete_response.text = ""
                    requests_delete_mock.return_value = delete_response
                    _, _, _, _ = self.api_caller.delete_resource("/resource", discovery_container)
                    self.assertNotIn("/resource", discovery_container)

    def test_get_xml(self):
        with mock.patch('requests.get') as get:
            response = Mock()
            response.status_code = ReturnCodes.OK
            response.headers = {}
            response.text = "<xml></xml>"
            get.return_value = response

            link, status, status_code, response_body, headers = self.api_caller.get_xml("uri")
        self.assertEqual("<xml></xml>", response_body)
示例#3
0
class MetadataPatchValidator(SkipListMixin, PatchNontrivialPropertyMixin):
    def __init__(self, metadata_container, configuration, strategy, requirements=None, skip_list=None):
        """
        :type metadata_container: cts_core.metadata.metadata_container.MetadataContainer
        :type configuration: cts_framework.configuration.configuration.Configurations
        :type strategy: cts_core.validation.patch.patching_strategy.PatchingStrategy
        :type requirements: list
        """
        self._metadata_container = metadata_container
        self._api_caller = ApiCaller(configuration)
        self._strategy = strategy
        self._preconditions = Preconditions(metadata_container, requirements)
        self._fast_mode = getenv('CTS_PATCH_ONE_PER_TYPE')
        self._types_patched = set()
        self._skip_list = skip_list

        if self._fast_mode:
            cts_warning("Test results may be UNRELIABLE - PATCHING ONLY ONE RESOURCE OF EACH TYPE!")

        self._endpoints_and_keys_to_ignore = []
        if self._skip_list:
            cts_warning("Test results may be UNRELIABLE - Some elements presented on REST API will be IGNORED!")
            self._endpoints_and_keys_to_ignore = self._load_ignore_list_file(skip_list)

    def validate(self, discovery_container):
        self.discovery_container = discovery_container
        status = self._check_mandatory_entities()

        for api_resource in self._enumerate_resources(discovery_container):
            context = Context(api_resource)
            resource_status = self._validate_resource(context)
            self._print_resource_footer(api_resource, context, resource_status)
            redfish_uri_compliance = discovery_container.validate_redfish_uris_consistency(api_resource, self._metadata_container)
            status = ValidationStatus.join_statuses(status, resource_status, redfish_uri_compliance)

        print "SCREEN::Overall status: %s" % status
        return status

    @staticmethod
    def _load_ignore_list_file(path_to_file=None):
        if not path_to_file:
            return []

        endpoints_to_ignore = {}
        with open(path_to_file, 'r') as f:
            for line in f.readlines():
                if '::' in line:
                    pre_formatted_value = line.split('::')
                else:
                    cts_warning("{line} is not proper format, add ::[*] to ignore entire endpoint")
                    continue
                list_of_endpoints_key = MetadataPatchValidator.__parse_into_a_list(pre_formatted_value[1])
                endpoints_to_ignore[pre_formatted_value[0]] = [value for value in list_of_endpoints_key.split(',')]
            print(endpoints_to_ignore)
        return endpoints_to_ignore

    @staticmethod
    def __parse_into_a_list(text):
        return text.replace('\n', '').replace('[', '').replace(']', '')

    def _enumerate_resources(self, discovery_container):
        count = len(discovery_container)
        resource_urls = sorted(self.discovery_container.keys())
        for idx, url in enumerate(resource_urls):
            api_resource = discovery_container[url]
            self._print_progress(api_resource, count, idx)
            if self.skip_resource(api_resource):
                continue
            if self._fast_mode:
                generalized_id = discovery_container._generalize_url(api_resource.url)
                if generalized_id in self._types_patched:
                    print "MESSAGE::skipping... other {} has already been tested". \
                        format(generalized_id)
                    continue
                else:
                    self._types_patched.add(generalized_id)

            yield api_resource

    @staticmethod
    def _print_progress(api_resource, count, idx):
        print "SCREEN::" + "-" * 120
        progress = "[%5d/%5d]" % (idx + 1, count)
        print "MESSAGE::%s - %s : %s" % \
              (progress, api_resource.odata_id, api_resource.odata_type)

    def _check_mandatory_entities(self):
        print "TEST_CASE::Checking for mandatory entities"
        status = self._preconditions.validate(self.discovery_container)
        print "STATUS::%s" % status
        return status

    def _validate_resource(self, context):
        """
        :type context: Context
        :rtype: str
        """
        api_resource = context.api_resource

        if self._metadata_container.to_be_ignored(api_resource.odata_type):
            return ValidationStatus.PASSED

        # Load Ignore Elements list and verify.
        properties_to_skip = []
        if api_resource.odata_id in self._endpoints_and_keys_to_ignore:
            properties_to_skip = self._endpoints_and_keys_to_ignore[api_resource.odata_id]

            if properties_to_skip[0] == '*':
                print('MESSAGE::Skipping patching {}. This odata_id is present on list of endpoints to ignore'.
                      format(api_resource.odata_id))
                return ValidationStatus.PASSED_WITH_WARNINGS
            else:
                print('MESSAGE::This properties from {} will be skipped from patching.'.
                      format(api_resource.odata_id))
                print('MESSAGE::Elements are on list to ignore:')
                for idp, property in enumerate(properties_to_skip, 1):
                    print('MESSAGE::\t{idp}. {property}'.format(idp=idp,
                                                                property=property))

        # do not attempt patch Virtual systems
        if "SystemType" in api_resource.body and api_resource.body["SystemType"] == "Virtual":
            print "MESSAGE::Skipping patching of a virtual system {}".format(api_resource.odata_id)
            return ValidationStatus.PASSED

        try:
            properties_list = [property for property in
                               self._metadata_container.entities[api_resource.odata_type].properties.values()
                               if str(property) not in properties_to_skip]

            try:
                if len(properties_to_skip):
                    return self._validate_property_list(context,
                                                        list(),
                                                        properties_list,
                                                        properties_to_skip)

                return self._validate_property_list(context,
                                                    list(),
                                                    properties_list)
            except SystemExit:
                raise
            except:
                cts_error("Unhandled exception {exception:stacktrace} "
                          "while handling resource {odata_id:id}",
                          exception=format_exc(),
                          odata_id=api_resource.odata_id)
                return ValidationStatus.FAILED
        except KeyError:
            cts_error("Unable to find definition of entity type {odata_type}",
                      odata_type=api_resource.odata_type)
            return ValidationStatus.FAILED

    def _validate_property_list(self, context, variable_path, property_list, ignore_list=[]):
        """
        :type context: Context
        :type variable_path: list [str or int]
        :type property_list: list[cts_core.metadata.model.property.Property]
        :rtype: str
        """
        api_resource = context.api_resource

        status = ValidationStatus.PASSED
        # use local copy - api resource will be modified while test is executed
        local_property_list = list(property_list)

        for property_description in local_property_list:

            if not len(ignore_list) or (len(ignore_list) and str(property_description) not in sum([ignored_property.split("->") for ignored_property in ignore_list], [])):
                status = ValidationStatus.join_statuses(self._validate_property(context,
                                                                                variable_path,
                                                                                property_description,
                                                                                ignore_list=[ignored_property for ignored_property in ignore_list]),
                                                        status)
                continue
            else:
                if len([ignored_subproperty for ignored_subproperty in ignored_property.split("->") for ignored_property in ignore_list]) > 1:
                   status = ValidationStatus.join_statuses(self._validate_property(context,
                                                                                   variable_path,
                                                                                   property_description,
                                                                                   ignore_list=[ignored_property.split("->")[1] for ignored_property in ignore_list]),
                                                           status)
                else:
                    status = ValidationStatus.join_statuses(self._validate_property(context,
                                                                                    variable_path,
                                                                                    property_description,
                                                                                    ignore_list=[ignored_property for ignored_property in ignore_list]),
                                                            status)

        # validate additional properties
        try:
            body = api_resource.get_value_from_path(variable_path)
        except KeyError as key:
            cts_warning("Unable to access {odataid:id}->{path}. Key={key}",
                        odataid=api_resource.odata_id, path="->".join(variable_path), key=key)
            body = None

        if body is not None:
            all_properties = set(body.keys())
            known_properties = set([property.name for property in local_property_list])
            additional_properties = all_properties - known_properties
            for additional_property_name in additional_properties:
                property_value = body[additional_property_name]
                if isinstance(property_value, dict) and "@odata.type" in property_value:
                    raw_soup = """
                    <Property Name="{name}" Type="{type}">
                        <Annotation Term="OData.Permissions"
                                    EnumMember="OData.Permission/ReadWrite"/>
                    </Property>
                    """
                    soup = BeautifulSoup(raw_soup.format(name=additional_property_name,
                                                         type=get_odata_type(property_value)),
                                         "lxml").find_all("property")[0]

                    adhoc_description = Property(self._metadata_container, None, soup)
                    status = ValidationStatus.join_statuses(
                        self._validate_property(context,
                                                variable_path,
                                                adhoc_description),
                        status)

        return status

    def _validate_property(self, context, variable_path, property_description,
                           skip_collection=False, ignore_list=[]):
        """
        :type context: Context
        :type variable_path: list [str or int]
        :type property_description: cts_core.metadata.model.property.Property
        :type skip_collection: bool
        :rtype: str
        """
        applicability = None
        if property_description.patch_status == PatchStatus.NONTRIVIAL:
            applicability, validation_result = self.handle_nontrivial(context, variable_path, property_description)
            if applicability == ApplicabilityTestResult.MATCHED:
                return validation_result
            if applicability in [ApplicabilityTestResult.NOT_MATCHED, ApplicabilityTestResult.SKIP_PROPERTY]:
                return validation_result

        api_resource = context.api_resource

        if not skip_collection:
            variable_path = variable_path + [property_description.name]

        try:
            property_body = api_resource.get_value_from_path(variable_path)
            if len(ignore_list) == 1 and ignore_list[0] in property_body:
                path_s = "->".join([str(segment) for segment in variable_path])
                cts_message("Patching %s->%s" % (api_resource.odata_id, path_s))
                cts_message("This property {ignore_element} is marked in IgnoredElement list as element to skip".format(
                    ignore_element=ignore_list[0]
                ))
        except KeyError as error:
            if property_description.is_required:
                path_s = "->".join([str(segment) for segment in variable_path])
                print "TEST_CASE::Patching %s->%s" % (api_resource.odata_id, path_s)
                cts_error("Unable to patch {error}", error=error)
                status = ValidationStatus.FAILED
                print "STATUS::%s" % status
            else:
                status = ValidationStatus.PASSED

            return status

        if property_description.is_collection and not skip_collection:
            status = ValidationStatus.PASSED

            for path_element, _ in enumerate(property_body):
                status = \
                    ValidationStatus.join_statuses(status,
                                                   self._validate_property(context,
                                                                           variable_path + [path_element],
                                                                           property_description,
                                                                           skip_collection=True,
                                                                           ignore_list=ignore_list))
            return status
        else:
            try:
                if self._metadata_container.types[property_description.type].type_category \
                        == MetadataTypeCategories.COMPLEX_TYPE:
                    return self._validate_property_list(context,
                                                        variable_path,
                                                        self._metadata_container.types[property_description.type].
                                                        properties.itervalues(),
                                                        ignore_list)
                else:
                    if str(property_description) in ignore_list:
                        return ValidationStatus.PASSED
                    return self._validate_leaf_property(context,
                                                        variable_path,
                                                        property_description)
            except KeyError as key:
                cts_error("Unable to find definition of type {type} referenced by {odata_id:id}",
                          type=key,
                          odata_id=api_resource.odata_id)
                return ValidationStatus.FAILED

    def _validate_leaf_property(self, context, variable_path, property_description):
        """
        :type context: Context
        :type variable_path: list [str or int]
        :type property_description: cts_core.metadata.model.property.Property
        :rtype: str
        """
        if property_description.patch_status in (PatchStatus.NOT_PATCHABLE, PatchStatus.NOT_DEFINED):
            return ValidationStatus.PASSED

        if property_description.patch_status == PatchStatus.NONTRIVIAL:
            applicability, validation_result = self.handle_nontrivial(context, variable_path, property_description)
            if applicability == ApplicabilityTestResult.MATCHED:
                return validation_result
            if applicability in [ApplicabilityTestResult.NOT_MATCHED, ApplicabilityTestResult.SKIP_PROPERTY]:
                return validation_result

        return self._validate_patchable_property(context, variable_path, property_description)

    def _validate_patchable_property(self, context, variable_path, property_description):
        """
        :type context: Context
        :type variable_path: list [str or int]
        :type property_description: cts_core.metadata.model.property.Property
        :rtype: str
        """
        api_resource = context.api_resource
        verify_only_response = property_description.patch_status == PatchStatus.WRITE_ONLY

        new_values_list = property_description.generate_values(property_description.annotations)
        allowed_values = api_resource.get_allowable_from_path(variable_path)
        if allowed_values:
            new_values_list = [value for value in new_values_list if value in allowed_values]

        collection = None
        if property_description.is_collection and len(variable_path) > 1:
            collection = api_resource.get_value_from_path(variable_path[:-1])

        try:
            value_pre = current_value = api_resource.get_value_from_path(variable_path)
        except KeyError:
            return ValidationStatus.PASSED

        total_status = ValidationStatus.PASSED

        for new_value in new_values_list:
            if new_value != current_value:
                status, current_value = \
                    self._patch_and_verify_property(context,
                                                    variable_path,
                                                    current_value,
                                                    new_value,
                                                    verify_only_response,
                                                    collection=collection,
                                                    property_description_type=property_description.name)

                total_status = ValidationStatus.join_statuses(total_status, status)

                if current_value is None:
                    try:
                        value_pre = current_value = api_resource.get_value_from_path(variable_path)
                    except KeyError:
                        return total_status

        if not allowed_values or value_pre in allowed_values:
            total_status = ValidationStatus.join_statuses(total_status,
                                                          self._restore_property(context,
                                                                                 value_pre,
                                                                                 variable_path,
                                                                                 collection=collection))
        else:
            print "MESSAGE::Original value '{value_pre}' is illegal. Will not _restore_property" \
                .format(value_pre=dumps(value_pre))

        return total_status

    def _patch_and_verify_property(self, context, variable_path, current_value, value_requested,
                                   verify_only_response=False, collection=None, property_description_type=None):
        """
        :type context: Context
        :type variable_path: list [str or int]
        :type current_value: *
        :type value_requested: *
        :type verify_only_response: bool
        """

        property = "%s[%s]" \
                   % (context.api_resource.odata_id, "->".join([str(path) for path in
                                                                variable_path]))

        patch_applied, status, validation_status = self._patch_property(context,
                                                                        property,
                                                                        current_value,
                                                                        value_requested,
                                                                        variable_path,
                                                                        collection=collection)
        # only _verify_property if the PATCH request succeeded and is not only writeable
        if status == RequestStatus.SUCCESS and patch_applied:
            if not verify_only_response:
                verify_status, current_value = self._verify_property(context,
                                                                     patch_applied,
                                                                     property,
                                                                     current_value,
                                                                     value_requested,
                                                                     variable_path,
                                                                     property_description_type=property_description_type)
                validation_status = ValidationStatus.join_statuses(validation_status, verify_status)
            else:
                validation_status = ValidationStatus.join_statuses(validation_status, ValidationStatus.PASSED)

        return validation_status, current_value

    def _patch_property(self, context, property_name, value_pre, value_requested, variable_path, collection=None):
        """
        Issues Patch request to update property to new requested value. Tries to determine
        if patch has been applied and sets validation status accordingly.

        :type context: Context
        :type property_name: str
        :type value_pre: *
        :type value_requested: *
        :type variable_path: list [str or int]
        :rtype: (bool, bool, str)
        """

        api_resource = context.api_resource

        print "TEST_CASE:: Patch %s := %s" % (property_name, str(value_requested))
        context.register_patch_attempt(property_name, str(value_requested))

        data = create_data(variable_path, value_requested, collection)

        status, status_code, response_body, headers = self._api_caller.patch_resource(
            api_resource.url,
            self.discovery_container,
            payload=data,
            acceptable_return_codes=self._strategy.allowable_return_codes)

        patch_applied = self._strategy.was_patch_applied(status_code)

        if status != RequestStatus.SUCCESS or not patch_applied:
            validation_status = ValidationStatus.FAILED
        else:
            validation_status = ValidationStatus.PASSED
        print "STATUS::%s" % validation_status

        return patch_applied, status, validation_status

    def _verify_property(self, context, patch_applied, property_name, value_pre,
                         value_requested, variable_path, property_description_type):
        """
        Verifies if property value after patch is as expected.
        Should be equal to original value if patch has not been applied.
        Should be equal to requested value if patch has been applied.

        :type context: Context
        :type patch_applied: bool
        :type property_name: str
        :type value_pre: *
        :type value_requested: *
        :type variable_path: list[str or int]
        :rtype: (str, *)
        """

        api_resource = self.discovery_container[context.api_resource.url] # refresh reference to the resource

        validation_status = ValidationStatus.PASSED
        print "TEST_CASE::Verify %s (expected: %s)" % (property_name, str(value_requested))

        try:
            value_post = api_resource.get_value_from_path(variable_path)

            if self._validate_corner_cases(property_description_type, value_post, value_requested) and patch_applied:
                print "STATUS::%s" % validation_status
                return validation_status, value_post

            if value_requested != value_post and patch_applied:
                cts_error("{odata_id:id} Unexpected value after patching {property} - " +
                          "IS : {post:ignore}, EXPECTED : {requested:ignore}",
                          odata_id=api_resource.odata_id,
                          property=property_name,
                          post=value_post,
                          requested=value_requested)
                validation_status = ValidationStatus.FAILED
            elif value_pre != value_post and not patch_applied:
                cts_error("{odata_id:id} Service reported that the patch has not been applied, "
                          "but '{property}' has changed unexpectedly "
                          "from {pre:ignore} to {post:ignore}",
                          odata_id=api_resource.odata_id,
                          property=property_name,
                          pre=value_pre,
                          post=value_post)
                validation_status = ValidationStatus.FAILED
        except ValueNotFound:
            validation_status = ValidationStatus.FAILED

        print "STATUS::%s" % validation_status
        return validation_status, value_post

    def _restore_property(self, context, value_pre, variable_path, collection=None):
        """
        Final patching that restores original value of the property.

        :type context: Context
        :type value_pre: *
        :type variable_path: list [int or str]
        :return: str
        """
        api_resource = context.api_resource

        validation_status = ValidationStatus.PASSED

        property = "%s" % ("->".join([str(path) for path in variable_path]))
        print "TEST_CASE::Restore %s := %s" % (property, dumps(value_pre))

        data = create_data(variable_path, value_pre, collection)
        status, status_code, response_body, headers = self._api_caller.patch_resource(
            api_resource.url,
            self.discovery_container,
            payload=data,
            acceptable_return_codes=self._strategy.allowable_return_codes)
        if status != RequestStatus.SUCCESS:
            cts_error("{odata_id:id} Restoring {property} failed. status code {code}",
                      odata_id=api_resource.odata_id,
                      property=property,
                      code=status_code)
            validation_status = ValidationStatus.FAILED
        print "STATUS::%s" % validation_status
        return validation_status

    def _print_resource_footer(self, api_resource, context, resource_status):
        if not context.attempted_patches:
            if resource_status not in [ValidationStatus.PASSED,
                                       ValidationStatus.PASSED_WITH_WARNINGS]:
                # 0 attempted patches means 0 test cases.
                # FAILED means error (visible in the log) happened during validation,
                # but not associated with any TEST_CASE
                # For the purpose of showing failing test case, we produce dummy TEST_CASE here
                print "TEST_CASE::Patching %s" % api_resource.odata_id
                print "STATUS::%s" % resource_status
            else:
                print "MESSAGE::[{resource} - 0 patchable properties found]". \
                    format(resource=api_resource.odata_id)

    def _validate_corner_cases(self, property_description_type, value_post, value_requested):
        if property_description_type == "MACAddress":
            return self._validate_canonical_form_of_mac_address(value_post, value_requested)
        return False

    def _validate_canonical_form_of_mac_address(self, value_post, value_requested):
        post = self._mac_address_standarizer(value_post)
        req = self._mac_address_standarizer(value_requested)

        return int(post, 16) == int(req, 16)

    @staticmethod
    def _mac_address_standarizer(mac_address):
        replace_dict = {":": "", "-": ""}
        for i, j in replace_dict.iteritems():
            mac_address = mac_address.replace(i, j)

        return mac_address
示例#4
0
class CRUDOperations(CtsTestScript):
    TIMEOUT = 600
    DESCRIPTION = """test script validating CRUD (create, read, update and delete) functionalities"""

    CONFIGURATION_PARAMETERS = [
        TestScript.ConfigurationParameter(
            parameter_name="UniqueInitiatorName",
            parameter_description="NVMe Qualified Name (NQN) for Initiator",
            parameter_type=str,
            is_required=True,
            parameter_default_value=""),
        TestScript.ConfigurationParameter(
            parameter_name="UniqueTargetName",
            parameter_description="NVMe Qualified Name (NQN) for Target",
            parameter_type=str,
            is_required=True,
            parameter_default_value="")
    ]

    def run(self):
        if self.metadata_container is None:
            return

        self.chosen_endpoint, self.chosen_zone, self.chosen_volume, self.chosen_volume_collection = (
            None, ) * 4
        self.initiator_endpoint, self.target_endpoint, self.zone_endpoint, self.created_volume = (
            None, ) * 4

        self.initiator_unique_name, self.target_unique_name = \
            self.configuration.UniqueInitiatorName, self.configuration.UniqueTargetName

        test_name = "Storage Services CRUD test with NVM Express (NVMe) Support"
        print "MESSAGE::%s starting" % test_name

        print "TEST_CASE::API crawling"
        api_explorer = ApiExplorer(self.metadata_container, self.configuration)
        self.discovery_container, status = api_explorer.discover(
            MetadataConstants.SERVICE_ROOT_URI, MetadataConstants.SERVICE_ROOT)
        print "STATUS::{status}".format(status=status)

        requirements = [
            Requirement(MetadataConstants.SERVICE_ROOT, min=1, max=1),
            Requirement(MetadataConstants.STORAGE_SERVICE, min=1),
            Requirement(MetadataConstants.VOLUME, min=1),
            Requirement(MetadataConstants.VOLUME_COLLECTION, min=1)
        ]
        preconditions = Preconditions(self.metadata_container, requirements)
        status = preconditions.validate(self.discovery_container)

        if status == ValidationStatus.FAILED:
            self.set_status_failed()
            return

        self.api_caller = ApiCaller(self.configuration)

        status = ValidationStatus.join_statuses(status, self.crud_nvme())

        print "MESSAGE::%s overall status: %s" % (
            test_name, ColorPrinter.format_status(status))

    def crud_nvme(self):
        status = ValidationStatus.PASSED

        cts_message("CRUD NVME")

        # test pre check
        self.__find_volumes_without_any_links(optional_pre_check=True)

        try:
            status = ValidationStatus.join_statuses(status,
                                                    self._test_create_volume())
        except:
            self._test_delete_all_created_endpoints_and_zones()
            status = ValidationStatus.FAILED

        self.__find_and_choose_endpoint_collection()
        self.__find_and_choose_volume()

        if not (self.chosen_volume and self.chosen_endpoint
                and self.chosen_zone):
            self.set_status_blocked()
            return ValidationStatus.BLOCKED

        cts_message("Chosen Volume for test: {volume_url}".format(
            volume_url=self.chosen_volume))

        try:
            status = ValidationStatus.join_statuses(
                status, self._test_create_initiator_endpoint())
            status = ValidationStatus.join_statuses(
                status, self._test_create_target_endpoint())
            status = ValidationStatus.join_statuses(
                status, self._test_create_zone_with_only_one_endpoint())
            status = ValidationStatus.join_statuses(
                status, self._test_patch_zone_with_target_endpoint())
        except:
            status = ValidationStatus.FAILED
        finally:
            status = ValidationStatus.join_statuses(
                status, self._test_delete_all_created_endpoints_and_zones())

        return status

    # Test case 1:
    def _test_create_volume(self):
        print "TEST_CASE::Create volume"

        post_volume_payload = dict(CapacityBytes=10000000)

        status, status_code, response_body, headers = self.api_caller.post_resource(
            self.chosen_volume_collection,
            self.discovery_container,
            payload=post_volume_payload,
            acceptable_return_codes=[
                ReturnCodes.OK, ReturnCodes.ACCEPTED, ReturnCodes.CREATED
            ])

        if not status:
            cts_error("CTS cannot create volume")
            self.set_status_failed()
            return ValidationStatus.FAILED

        self.created_volume = headers['location']
        cts_message("Volume created at {location}".format(
            location=self.created_volume))

        if not self._verify_size_of_created_volume(self.created_volume,
                                                   post_volume_payload):
            cts_error("Service create volume with wrong capacity")
            self.set_status_failed()
            return ValidationStatus.FAILED

        self.set_status_passed()
        return ValidationStatus.PASSED

    # Test case 2:
    def _test_create_initiator_endpoint(self):
        print "TEST_CASE::Create initiator endpoint"

        post_initiator_endpoint_payload = dict(
            Identifiers=[
                dict(DurableName=self.initiator_unique_name,
                     DurableNameFormat="NQN")
            ],
            IPTransportDetails=[],
            ConnectedEntities=[dict(
                EntityRole="Initiator",
                Identifiers=[],
            )],
            Links=dict(Oem=dict(Intel_RackScale=dict())))

        status, status_code, response_body, headers = self.api_caller.post_resource(
            self.chosen_endpoint,
            self.discovery_container,
            payload=post_initiator_endpoint_payload,
            acceptable_return_codes=[
                ReturnCodes.OK, ReturnCodes.ACCEPTED, ReturnCodes.CREATED
            ])

        if not status:
            cts_error("CTS cannot create Initiator Endpoint")
            self.set_status_failed()
            return ValidationStatus.FAILED

        self.initiator_endpoint = headers['location']
        cts_message("Initiator Endpoint created at {location}".format(
            location=self.initiator_endpoint))

        try:
            verify = self.__verify_created_initiator_endpoint(
                self.initiator_endpoint, post_initiator_endpoint_payload)
        except:
            verify = False

        if not verify:
            cts_error("Service create initiator endpoint with wrong value")
            self.set_status_failed()
            return ValidationStatus.FAILED

        self.set_status_passed()
        return ValidationStatus.PASSED

    # Test case 3:
    def _test_create_target_endpoint(self):
        print "TEST_CASE::Create target endpoint"

        selected_volume = self.discovery_container.get(self.chosen_volume)
        post_target_endpoint_payload = dict(
            Identifiers=[
                dict(DurableName=self.target_unique_name,
                     DurableNameFormat="NQN")
            ],
            IPTransportDetails=[],
            ConnectedEntities=[
                dict(
                    EntityRole="Target",
                    Identifiers=[],
                    EntityLink={"@odata.id": selected_volume.odata_id},
                )
            ],
            Links=dict(Oem=dict(Intel_RackScale=dict())))

        status, status_code, response, headers = self.api_caller.post_resource(
            self.chosen_endpoint,
            self.discovery_container,
            payload=post_target_endpoint_payload,
            acceptable_return_codes=[
                ReturnCodes.OK, ReturnCodes.ACCEPTED, ReturnCodes.CREATED
            ])

        if not status:
            cts_error("CTS can not create Target Endpoint")
            self.set_status_failed()
            return ValidationStatus.FAILED

        self.target_endpoint = headers['location']
        cts_message("Target Endpoint created at {location}".format(
            location=self.target_endpoint))

        try:
            verify = self.__verify_created_target_endpoint(
                self.target_endpoint, post_target_endpoint_payload)
        except:
            verify = False

        if not verify:
            cts_error("Service create target endpoint with wrong value")
            self.set_status_failed()
            return ValidationStatus.FAILED

        self.set_status_passed()
        return ValidationStatus.PASSED

    # Test case 4:
    def _test_create_zone_with_only_one_endpoint(self):
        print "TEST_CASE::Create zone with only one endpoint"

        try:
            initiator_endpoint_link = self.__get_odata_id_links(
                self.initiator_endpoint)
        except:
            cts_error("No valid initiator endpoint for zone creation")
            self.set_status_blocked()
            return ValidationStatus.BLOCKED

        post_create_zone_payload = dict(Links=dict(
            Endpoints=[{
                "@odata.id": initiator_endpoint_link
            }]))

        status, status_code, _, headers = self.api_caller.post_resource(
            self.chosen_zone,
            self.discovery_container,
            payload=post_create_zone_payload,
            acceptable_return_codes=[
                ReturnCodes.ACCEPTED, ReturnCodes.CREATED, ReturnCodes.OK
            ])

        if not status:
            cts_error("CTS can not create Zone")
            self.set_status_failed()
            return ValidationStatus.FAILED

        self.zone_endpoint = headers['location']
        cts_message(
            "Zone created at {location}".format(location=self.zone_endpoint))

        if not self.__verify_created_zone(self.zone_endpoint,
                                          post_create_zone_payload):
            cts_error("Service zone created with wrong value")
            self.set_status_failed()
            return ValidationStatus.FAILED

        self.set_status_passed()
        return ValidationStatus.PASSED

    # Test case 5:
    def _test_patch_zone_with_target_endpoint(self):
        print "TEST_CASE::Patch zone with target endpoint"

        initiator_or_target_missing = False

        try:
            initiator_endpoint_link = self.__get_odata_id_links(
                self.initiator_endpoint)
        except:
            cts_error("No valid initiator endpoint for zone patching")
            initiator_or_target_missing = True

        try:
            target_endpoint_link = self.__get_odata_id_links(
                self.target_endpoint)
        except:
            cts_error("No valid target endpoint for zone patching")
            initiator_or_target_missing = True

        if initiator_or_target_missing:
            cts_error("Missing resources for zone patching")
            self.set_status_blocked()

            return ValidationStatus.BLOCKED

        patch_create_zone_payload = dict(Links=dict(
            Endpoints=[{
                "@odata.id": initiator_endpoint_link
            }, {
                "@odata.id": target_endpoint_link
            }]))

        status, status_code, _, headers = self.api_caller.patch_resource(
            self.zone_endpoint,
            self.discovery_container,
            payload=patch_create_zone_payload,
            acceptable_return_codes=[ReturnCodes.ACCEPTED, ReturnCodes.OK])
        if not status:
            cts_error("CTS can not PATCH Zone")
            self.set_status_failed()
            return ValidationStatus.FAILED

        cts_message(
            "Zone PATCHED at {location}".format(location=self.zone_endpoint))

        if not self.__verify_patched_zone(self.zone_endpoint,
                                          patch_create_zone_payload):
            cts_error("Service zone patched with wrong value")
            self.set_status_failed()
            return ValidationStatus.FAILED

        self.set_status_passed()
        return ValidationStatus.PASSED

    # Test case 6:
    def _test_delete_all_created_endpoints_and_zones(self):
        status = ValidationStatus.PASSED
        for resource, link in OrderedDict([
            ("volume", self.created_volume), ("zone", self.zone_endpoint),
            ("initiator endpoint", self.initiator_endpoint),
            ("target endpoint", self.target_endpoint)
        ]).iteritems():
            print "TEST_CASE::Delete {resource}".format(resource=resource)
            if link:
                if not self.__delete_location(link):
                    self.set_status_failed()
                    status = ValidationStatus.join_statuses(
                        status, ValidationStatus.FAILED)
                else:
                    self.set_status_passed()
                    status = ValidationStatus.join_statuses(
                        status, ValidationStatus.PASSED)
            else:
                self.set_status_blocked()
                status = ValidationStatus.join_statuses(
                    status, ValidationStatus.BLOCKED)
        return status

    def __check_resources_have_same_parent_url(self, res1, res2):
        return (self.discovery_container.get(res1)).parent_url == (
            self.discovery_container.get(res2)).parent_url

    def __choose_endpoint_and_zone_from_fabric_collection(
            self, endpoint_collection, zone_collection):
        for ec in endpoint_collection:
            for zc in zone_collection:
                if self.__check_resources_have_same_parent_url(ec, zc):
                    return ec, zc
        return None, None

    def __find_and_choose_volume(self):
        for volume in self.__find_volumes_without_any_links():
            self.chosen_volume = volume
            break

    def __delete_location(self, location_url):
        status, _, _, _ = self.api_caller.delete_resource(
            location_url,
            self.discovery_container,
            acceptable_return_codes=[ReturnCodes.NO_CONTENT])
        if not status:
            cts_error("Resource at %s was not properly deleted" % location_url)
            return False
        return self.__verify_resource_non_exist(location_url)

    def __verify_resource_non_exist(self, location_url_verification):
        status, _, _, _, _ = self.api_caller.get_resource(
            location_url_verification,
            self.discovery_container,
            acceptable_return_codes=[
                ReturnCodes.BAD_REQUEST, ReturnCodes.NOT_FOUND
            ])
        if not status:
            cts_error("Resource at %s was not properly deleted" %
                      location_url_verification)
            return False
        cts_message("Resource at %s was properly deleted" %
                    location_url_verification)
        return True

    def __find_and_choose_endpoint_collection(self):
        endpoint_collection = dict(
            self.discovery_container.get_resources(
                MetadataConstants.ENDPOINT_COLLECTION))

        zones_collection = dict(
            self.discovery_container.get_resources(
                MetadataConstants.ZONE_COLLECTION))

        if not endpoint_collection:
            cts_error("No Endpoint Collection")
            self.set_status_blocked()

        if not zones_collection:
            cts_error("No Zone Collection")
            self.set_status_blocked()

        self.chosen_endpoint, self.chosen_zone = \
            self.__choose_endpoint_and_zone_from_fabric_collection(
                    endpoint_collection,
                    zones_collection)

    def __find_volumes_without_any_links(self, optional_pre_check=False):
        reusable_volumes = []
        logical_drive_collections = dict(
            self.discovery_container.get_resources(
                MetadataConstants.VOLUME_COLLECTION, any_child_version=True))

        # Allocated Volumes are connected with this same type as Volume
        volumes_collection_wo_allocated_volumes = [
            x for x in logical_drive_collections if not "AllocatedVolumes" in x
        ]

        for collection in volumes_collection_wo_allocated_volumes:
            if not self.chosen_volume_collection:
                self.chosen_volume_collection = collection
            logical_drive_groups = dict(
                self.discovery_container.get_resources(
                    MetadataConstants.VOLUME, any_child_version=True))
            volumes_collection = []

            for c in logical_drive_groups.keys():
                try:
                    # this Volume we can use to our tests
                    if len(
                            self.discovery_container.get(c).body["Links"]
                        ["Oem"]["Intel_RackScale"]["Endpoints"]) == 0:
                        volumes_collection.append(c)
                except KeyError:
                    pass

            # if there are any free Volume, suspend test
            if len(volumes_collection) == 0:
                if optional_pre_check:
                    return False
                cts_error(
                    "No reusable Volume. Create a Volume without linked Endpoints"
                )
                self.set_status_blocked()
            else:
                reusable_volumes.extend(volumes_collection)
        return reusable_volumes

    def __get_odata_id_links(self, link):
        return (self.discovery_container.get(link)).odata_id

    # verification code
    def __verify_patched_zone(self, resource_location, resource_payload):
        print "MESSAGE::Comparing newly patched zone to given payload"
        return self.__verify_zone(resource_location, resource_payload)

    def __verify_created_zone(self, resource_location, resource_payload):
        print "MESSAGE::Comparing newly created zone to given payload"
        return self.__verify_zone(resource_location, resource_payload)

    def __verify_zone(self, resource_location, resource_payload):
        location_payload_to_compare = JsonComparator.odict_to_dict(
            self.discovery_container.get(resource_location).body["Links"]
            ["Endpoints"])
        payload_to_compare = resource_payload["Links"]["Endpoints"]

        return JsonComparator.compare_lists(location_payload_to_compare,
                                            payload_to_compare)

    def __verify_created_target_endpoint(self, resource_location,
                                         resource_payload):
        print "MESSAGE::Comparing newly created target endpoint to given payload"

        location_payload_to_compare = JsonComparator.odict_to_dict(
            self.discovery_container.get(resource_location).body)
        payload_to_compare = resource_payload

        verification_dict = {
            'ConnectedEntities': ['EntityLink', 'EntityRole'],
            'Identifiers': ['DurableNameFormat', 'DurableName']
        }

        return self.__verify_body(location_payload_to_compare,
                                  payload_to_compare, verification_dict)

    def __verify_created_initiator_endpoint(self, resource_location,
                                            resource_payload):
        print "MESSAGE::Comparing newly created initiator endpoint to given payload"
        location_payload_to_compare = JsonComparator.odict_to_dict(
            self.discovery_container.get(resource_location).body)
        payload_to_compare = resource_payload

        verification_dict = {
            'ConnectedEntities': ['EntityRole'],
            'Identifiers': ['DurableNameFormat', 'DurableName']
        }

        return self.__verify_body(location_payload_to_compare,
                                  payload_to_compare, verification_dict)

    def _verify_size_of_created_volume(self, resource_location,
                                       resource_payload):
        print "MESSAGE::Comparing newly created volume to given payload"

        discovery_service_capacity = self.discovery_container.get(
            resource_location).body["CapacityBytes"]
        resource_payload_capacity = resource_payload["CapacityBytes"]

        return self.__verify_bytes(resource_payload_capacity,
                                   discovery_service_capacity)

    @staticmethod
    def __verify_body(resource_1, resource_2, verification_dict):
        try:
            for verification_key in verification_dict.keys():
                for verification_elem in verification_dict[verification_key]:
                    if resource_1[verification_key][0][verification_elem] != \
                            resource_2[verification_key][0][verification_elem]:
                        return False
        except KeyError:
            return False
        return True

    @staticmethod
    def __verify_bytes(resource_on_payload,
                       resource_discovered,
                       min_bytes=None,
                       max_bytes=None):
        if not min_bytes:
            # default 4 mb
            min_bytes = 4000000
        if not max_bytes:
            # default 4 mb
            max_bytes = 4000000

        if (resource_on_payload - min_bytes) <= resource_discovered <= (
                resource_on_payload + max_bytes):
            return True
        return False

    @staticmethod
    def __verify_strings(resource_on_payload, resource_discovered):
        resource_on_payload = str(resource_on_payload).lower()
        resource_discovered = str(resource_discovered).lower()

        return resource_on_payload == resource_discovered
示例#5
0
class CRUDOperations(CtsTestScript):
    TIMEOUT = 600
    DESCRIPTION = """test script validating CRUD (create, read, update and delete) functionalities"""

    def run(self):
        if self.metadata_container is None:
            return

        test_name = "Storage Services CRUD test"
        print "MESSAGE::%s starting" % test_name

        print "TEST_CASE::API crawling"
        api_explorer = ApiExplorer(self.metadata_container, self.configuration)
        self.discovery_container, status = api_explorer.discover(MetadataConstants.SERVICE_ROOT_URI,
                                                                 MetadataConstants.SERVICE_ROOT)
        print "STATUS::{status}".format(status=status)

        requirements = [
            Requirement(MetadataConstants.SERVICE_ROOT, min=1, max=1),
            Requirement(MetadataConstants.STORAGE_SERVICE, min=1),
            Requirement(MetadataConstants.LOGICAL_DRIVE_COLLECTION, min=1),
            Requirement(MetadataConstants.REMOTE_TARGET_COLLECTION, min=1),
            Requirement(MetadataConstants.LOGICAL_DRIVE, min=2)
        ]
        preconditions = Preconditions(self.metadata_container, requirements)
        status = preconditions.validate(self.discovery_container)

        if  status == ValidationStatus.FAILED:
            self.set_status_blocked()
            return

        self.api_caller = ApiCaller(self.configuration)

        status = ValidationStatus.join_statuses(status, self.crud_logical_drive())
        status = ValidationStatus.join_statuses(status, self.crud_remote_target())

        print "MESSAGE::%s overall status: %s" % (test_name, ColorPrinter.format_status(status))



    def _test_case_create_logical_drive(self, logical_drive_collection, payload=None):
        print "TEST_CASE::Create a logical drive"

        if not payload:
            payload = self.post_logical_drive_payload
        status, status_code, response_body, headers = self.api_caller.post_resource(
            logical_drive_collection, self.discovery_container, payload=payload)

        if not status:
            cts_error("Wrong status code of POST response: {status_code}; response body: {response_body:response_body}",
                      **locals())
            self.set_status_failed()
            return ValidationStatus.FAILED

        try:
            self.created_logical_drive = headers["Location"]
            self.created_logical_drive_netloc = \
                self.discovery_container.get_netloc(logical_drive_collection)
            print "MESSAGE::Newly created logical drive url %s" % self.created_logical_drive
        except KeyError:
            cts_error("In header shall be provided location to created resource")
            self.set_status_failed()
            return ValidationStatus.FAILED

        print "MESSAGE::Logical drive created"
        self.set_status_passed()
        return ValidationStatus.PASSED

    def _test_case_get_created_logical_drive(self, logical_drive):
        print "TEST_CASE::Get the created logical drive"

        link, status, status_code, response_body, _ = \
            self.api_caller.get_resource(self.created_logical_drive,
                                         self.discovery_container,
                                         api_endpoint_override=self.created_logical_drive_netloc)

        if not status:
            cts_error("Wrong status code of GET response: {status_code}; response body: {response_body:response_body}",
                      **locals())
            self.set_status_failed()
            return ValidationStatus.FAILED
        else:
            print "MESSAGE::Comparing newly created logical drive to given payload"
            if not JsonComparator.compare_json_to_golden(response_body, self.expected_created_logical_drive_body,
                                                         ignore = ["/Name"]): # Name setting not supported by PSME REST API
                cts_error("Newly created logical drive's body incorrect")
                self.set_status_failed()
                print "MESSAGE::Proceeding with deleting the resource"
                return ValidationStatus.FAILED

        print "MESSAGE::Newly created logical drive's correct"
        self.set_status_passed()
        return ValidationStatus.PASSED

    def _test_case_update_created_logical_drive(self):
        print "TEST_CASE::Update the created logical drive"

        patch_logical_drive_payload = dict(
            Bootable = not self.post_logical_drive_payload["Bootable"]
        )

        status, status_code, response_body, _ = self.api_caller.patch_resource(self.created_logical_drive,
                                                                               self.discovery_container,
                                                                               payload=patch_logical_drive_payload)

        if not status:
            cts_error("Error while executing PATCH; status code: {status_code}; response body: "
                      "{response_body:response_body}",
                      **locals())
            self.set_status_failed()
            return ValidationStatus.FAILED

        link, status, status_code, response_body, _ = \
            self.api_caller.get_resource(self.created_logical_drive,
                                         self.discovery_container,
                                         api_endpoint_override=self.created_logical_drive_netloc)

        if not status:
            cts_error("Wrong status code of GET response: {status_code}; response body: {response_body:response_body}",
                      **locals())
            self.set_status_failed()
            return ValidationStatus.FAILED
        else:
            print "MESSAGE::Verifying whether the logical drive's update has taken place"
            if not JsonComparator.compare_json_to_golden(response_body["Bootable"],
                                                         patch_logical_drive_payload["Bootable"]):
                cts_error("Newly created logical drive could not be updated")
                self.set_status_failed()
                print "MESSAGE::Proceeding with deleting the resource"
                return ValidationStatus.FAILED

        print "MESSAGE::Newly created logical drive updated correctly"
        self.set_status_passed()
        return ValidationStatus.PASSED

    def _test_case_delete_created_logical_drive(self, logical_drive):
        print "TEST_CASE::Delete the created logical drive"

        status, status_code, response_body, _ = self.api_caller.delete_resource(self.created_logical_drive,
                                                                                self.discovery_container)

        if not status:
            cts_error("Wrong status code of DELETE response: {status_code}; response body: "
                      "{response_body:response_body}", **locals())
            self.set_status_failed()
            return ValidationStatus.FAILED

        print "MESSAGE::Newly created logical drive deleted"
        self.set_status_passed()
        return ValidationStatus.PASSED

    def _test_case_get_deleted_logical_drive(self, logical_drive):
        print "TEST_CASE::Get the deleted logical drive"

        link, status, status_code, response_body, _ = \
            self.api_caller.get_resource(self.created_logical_drive,
                                         self.discovery_container,
                                         acceptable_return_codes=[ReturnCodes.NOT_FOUND],
                                         api_endpoint_override=self.created_logical_drive_netloc)

        if not status:
            cts_error("Wrong status code of GET response after deletion: {status_code}; response body: "
                      "{response_body:response_body}", **locals())
            self.set_status_failed()
            return ValidationStatus.FAILED

        print "MESSAGE::Newly created logical drive not found (as intended)"
        self.set_status_passed()
        return ValidationStatus.PASSED


    def crud_logical_drive(self, create_logical_drive_for_target_crud=None):
        """
        Test is trying to perform CRUD (create, read, update, delete) operations on a logical drive
        :type create_logical_drive_for_target_crud: Boolean
        """

        status = ValidationStatus.BLOCKED

        lvm_found = False

        chosen_logical_drive_type, chosen_logical_drive_mode = None, None
        chosen_logical_drive_collection, chosen_logical_drive, chosen_logical_drive_group = None, None, None
        inherited_bootable = None

        # to speed things up, always look for the lowest capacity logical drive
        LVM_LV_SPECIFIC_CONSTRAINTS = [equal("Mode", "LV"), greater("CapacityGiB", 0), not_equal("Snapshot", True),
                                       order_by("CapacityGiB")]
        CEPH_RBD_SPECIFIC_CONSTRAINTS = [equal("Mode", "RBD"), greater("CapacityGiB", 0), order_by("CapacityGiB")]

        logical_drive_collections = dict(self.discovery_container.get_resources(
                                         MetadataConstants.LOGICAL_DRIVE_COLLECTION,
                                         any_child_version=True))

        for collection in logical_drive_collections.keys():
            chosen_logical_drive_collection = collection
            logical_drive_groups = dict(self.discovery_container.get_resources(MetadataConstants.LOGICAL_DRIVE,
                                                                      any_child_version=True,
                                                                      constraints=[equal("Mode", "LVG"),
                                                                                   from_collection(collection)]))
            if logical_drive_groups:
                lvm_found = True
            else:
                logical_drive_groups = dict(self.discovery_container.get_resources(MetadataConstants.LOGICAL_DRIVE,
                                                                                    any_child_version=True,
                                                                                    constraints=[equal("Mode", "Pool"),
                                                                                                 from_collection(
                                                                                                     collection)]))
            if logical_drive_groups:
                specific_constraints = LVM_LV_SPECIFIC_CONSTRAINTS if lvm_found else CEPH_RBD_SPECIFIC_CONSTRAINTS

                for lvg_link, lvg in logical_drive_groups.iteritems():
                    chosen_logical_drive_group = lvg.odata_id
                    try:
                        child_lvs = [lv["@odata.id"] for lv in lvg.body["Links"]["LogicalDrives"]]
                        chosen_logical_drive, _ = self.discovery_container.get_resources(
                                                                                       MetadataConstants.LOGICAL_DRIVE,
                                                                                       any_child_version=True,
                                                                                       constraints=specific_constraints
                                                                                       + [odata_ids(child_lvs)])[0]
                        lv = self.discovery_container[chosen_logical_drive]
                        inherited_bootable = lv.body["Bootable"]
                        chosen_logical_drive_type = lv.body["Type"]
                        chosen_logical_drive_mode = lv.body["Mode"]
                    except (KeyError,IndexError) as error:
                        cts_error("Exception while parsing {id:id}: {error:exception}", id=chosen_logical_drive,
                                                                                        error=error)
                        chosen_logical_drive = None
                        pass
                    else:
                        break
            if not chosen_logical_drive_group:
                resource = "logical drive group" if lvm_found else "Pool"
                cts_warning("Could not find a {resource} resource in collection {collection}", **locals())
            if lvm_found and not chosen_logical_drive:
                cts_warning("Could not find a logical volume in collection {collection}", **locals())

        if lvm_found and not chosen_logical_drive:
            cts_error("Could not find any logical volume")
            return status

        if not chosen_logical_drive_group:
            cts_error("Could not find any logical drive group, aborting")
            return status

        self.post_logical_drive_payload = dict(
            Name="LVM Logical Drive created by CTS",
            Type=chosen_logical_drive_type,
            Mode=chosen_logical_drive_mode,
            Protected=False,
            CapacityGiB=self.discovery_container[chosen_logical_drive].body["CapacityGiB"],
            Image="CTS test image",
            Bootable=inherited_bootable if inherited_bootable else False,
            Snapshot=True,
            Links=dict(
                LogicalDrives=[{"@odata.id": chosen_logical_drive_group}],
                MasterDrive={"@odata.id": urlparse(chosen_logical_drive).path}
            )
        )

        # Creating the first RBD if no other was found
        if not chosen_logical_drive:
            if lvm_found:
                print "ERROR::Could not find a logical drive suitable for snapshot"
                return status
            cts_warning("Could not find any logical drive suitable for snapshot. Since CEPH was detected, CTS will "\
                        "try to create the first RBD")
            self.post_logical_drive_payload = dict(
                Name="CEPH Logical Drive created by CTS",
                Type="CEPH",
                Mode="RBD",
                Protected=False,
                CapacityGiB=self.discovery_container[chosen_logical_drive_group].body["CapacityGiB"]/2,
                Bootable=False,
                Snapshot=False,
                Links=dict(
                    LogicalDrives=[{"@odata.id": urlparse(chosen_logical_drive_group).path}],
                    MasterDrive=None
                )
            )

        print "MESSAGE::Logical drive CRUD test will be performed on logical drive collection %s, creating logical " \
              "drive based on %s on group %s" % (chosen_logical_drive_collection, chosen_logical_drive,
                                                 chosen_logical_drive_group)

        self.expected_created_logical_drive_body = deepcopy(self.post_logical_drive_payload)
        # The logical drive in LogicalDrives link is really the volume group and is going to be seen under
        # UsedBy on REST
        self.expected_created_logical_drive_body["Links"]["UsedBy"] = \
            self.expected_created_logical_drive_body["Links"]["LogicalDrives"]
        self.expected_created_logical_drive_body["Links"]["LogicalDrives"] = []

        status = self._test_case_create_logical_drive(chosen_logical_drive_collection)
        if status == ValidationStatus.PASSED: # only perform other tests if creation was successful
            ld = self.created_logical_drive
            status = ValidationStatus.join_statuses(status, self._test_case_get_created_logical_drive(ld))
            if create_logical_drive_for_target_crud:
                return status
            if status == ValidationStatus.PASSED:
                status = ValidationStatus.join_statuses(status, self._test_case_update_created_logical_drive())
            status = ValidationStatus.join_statuses(status, self._test_case_delete_created_logical_drive(ld))
            status = ValidationStatus.join_statuses(status, self._test_case_get_deleted_logical_drive(ld))

        return status


    def _test_case_create_remote_target(self, remote_target_collection):
        print "TEST_CASE::Create a remote target"

        status, status_code, response_body, headers = self.api_caller.post_resource(
            remote_target_collection, self.discovery_container, payload=self.post_remote_target_payload)

        if not status:
            cts_error("Wrong status code of POST response: {status_code}; response body: {response_body:response_body}",
                      **locals())
            self.set_status_failed()
            return ValidationStatus.FAILED

        try:
            self.created_remote_target = headers["Location"]
            self.created_remote_target_netloc = \
                self.discovery_container.get_netloc(remote_target_collection)
            print "MESSAGE::Newly created remote target url %s" % self.created_remote_target
        except KeyError:
            cts_error("In header shall be provided location to created resource")
            self.set_status_failed()
            return ValidationStatus.FAILED

        print "MESSAGE::Remote target created"
        self.set_status_passed()
        return ValidationStatus.PASSED

    def _test_case_get_created_remote_target(self):
        print "TEST_CASE::Get the created remote target"

        link, status, status_code, response_body, _ = \
            self.api_caller.get_resource(self.created_remote_target,
                                         self.discovery_container,
                                         api_endpoint_override=self.created_remote_target_netloc)

        if not status:
            cts_error("Wrong status code of GET response: {status_code}; response body: {response_body:response_body}",
                      **locals())
            self.set_status_failed()
            return ValidationStatus.FAILED
        else:
            print "MESSAGE::Comparing newly created remote target to given payload"
            if not JsonComparator.compare_json_to_golden(response_body, self.post_remote_target_payload):
                cts_error("Newly created remote target's body incorrect")
                self.set_status_failed()
                print "MESSAGE::Proceeding with deleting the resource"
                return ValidationStatus.FAILED

        print "MESSAGE::Newly created remote target's body correct"
        self.set_status_passed()
        return ValidationStatus.PASSED

    def _test_case_logical_drive_has_link_to_created_target(self, logical_drive):
        print "TEST_CASE::Check for a link to the created target in logical drive's body"

        link, status, status_code, response_body, _ = \
            self.api_caller.get_resource(logical_drive,
                                         self.discovery_container)

        if not status:
            cts_error("Wrong status code of GET response: {status_code}; response body: {response_body:response_body}",
                      **locals())
            self.set_status_failed()
            return ValidationStatus.FAILED
        else:
            try:
                target = urlparse(self.created_remote_target).path
                if not {"@odata.id": target} in response_body["Links"]["Targets"]\
                   and not {"@odata.id": target + "/"} in response_body["Links"]["Targets"]:
                    cts_error("No link to the created target in the logical drive's body")
                    self.set_status_failed()
                    return ValidationStatus.FAILED
            except KeyError:
                cts_error("No Links/Targets array present in the logical drive's body")
                self.set_status_failed()
                return ValidationStatus.FAILED

        print "MESSAGE::A link from the logical drive to the target is present"
        self.set_status_passed()
        return ValidationStatus.PASSED

    def _test_case_update_created_remote_target(self):
        print "TEST_CASE::Update the created remote target"

        patch_remote_target_payload = dict(
            Initiator=[dict(
                iSCSI=dict(
                    InitiatorIQN="cts.initiator:patched_cts_initiator"
                )
            )]
        )

        status, status_code, response_body, _ = self.api_caller.patch_resource(self.created_remote_target,
                                                                               self.discovery_container,
                                                                               payload=patch_remote_target_payload)

        if not status:
            cts_error("Error while executing PATCH; status code: {status_code}; response body: "
                      "{response_body:response_body}",
                      **locals())
            self.set_status_failed()
            return ValidationStatus.FAILED

        link, status, status_code, response_body, _ = \
            self.api_caller.get_resource(self.created_remote_target,
                                         self.discovery_container,
                                         api_endpoint_override=self.created_remote_target_netloc)

        if not status:
            cts_error("Wrong status code of GET response: {status_code}; response body: {response_body:response_body}",
                      **locals())
            self.set_status_failed()
            return ValidationStatus.FAILED
        else:
            print "MESSAGE::Verifying whether the remote target's update has taken place"
            if not JsonComparator.compare_json_to_golden(response_body["Initiator"],
                                                         patch_remote_target_payload["Initiator"]):
                cts_error("Newly created remote target could not be updated")
                self.set_status_failed()
                print "MESSAGE::Proceeding with deleting the resource"
                return ValidationStatus.FAILED

        print "MESSAGE::Newly created remote target updated correctly"
        self.set_status_passed()
        return ValidationStatus.PASSED

    def _test_case_delete_created_remote_target(self):
        print "TEST_CASE::Delete the created remote target"

        status, status_code, response_body, _ = self.api_caller.delete_resource(self.created_remote_target,
                                                                                self.discovery_container)

        if not status:
            cts_error("Wrong status code of DELETE response: {status_code}; response body: "
                      "{response_body:response_body}",
                      **locals())
            self.set_status_failed()
            return ValidationStatus.FAILED

        print "MESSAGE::Newly created remote target deleted"
        self.set_status_passed()
        return ValidationStatus.PASSED

    def _test_case_get_deleted_remote_target(self):
        print "TEST_CASE::Get the deleted remote target"

        link, status, status_code, response_body, _ = \
            self.api_caller.get_resource(self.created_remote_target,
                                         self.discovery_container,
                                         acceptable_return_codes=[ReturnCodes.NOT_FOUND],
                                         api_endpoint_override=self.created_remote_target_netloc)

        if not status:
            cts_error("Wrong status code of GET response: {status_code}; response body: {response_body:response_body}",
                      **locals())
            self.set_status_failed()
            return ValidationStatus.FAILED

        print "MESSAGE::Newly created remote target not found (as intended)"
        self.set_status_passed()
        return ValidationStatus.PASSED

    def crud_remote_target(self):
        """
        Test is checking for rsa compute blade presence
        """

        ld_created_for_crud = False
        target_iqn = "cts.target:cts_test_target"
        target_iqns = set()

        chosen_logical_drive, chosen_remote_target_collection = None, None

        if not self.discovery_container.count_resources(MetadataConstants.LOGICAL_DRIVE, any_child_version=True):
            cts_error("Insufficient resources for API test. No logical drives found.")
            return ValidationStatus.BLOCKED

        logical_drives = dict(self.discovery_container.get_resources(MetadataConstants.LOGICAL_DRIVE,
                                                                      any_child_version=True,
                                                                      constraints=[equal("Mode", "LV"),
                                                                                   equal("Protected", False)]))
        if not logical_drives:
            logical_drives = dict(self.discovery_container.get_resources(MetadataConstants.LOGICAL_DRIVE,
                                                                      any_child_version=True,
                                                                      constraints=[equal("Mode", "RBD"),
                                                                                   equal("Protected", False)]))
        try:
            chosen_remote_target_collection = self.discovery_container.get_resources(
                                              MetadataConstants.REMOTE_TARGET_COLLECTION,
                                              any_child_version=True)[0][0]
            chosen_logical_drive = [link for link, resource in logical_drives.iteritems() if not
                                     len(resource.body["Links"]["Targets"])][0]
        except (KeyError, IndexError):
            pass

        if not chosen_remote_target_collection:
            cts_error("Insufficient resources available on API for test. Could not find any remote target collection")
            return ValidationStatus.BLOCKED

        if not chosen_logical_drive:
            cts_warning("Insufficient resources available on API for test. Could not find an unprotected logical drive "
                        "with no targets attached")
            print "MESSAGE::Trying to create a new logical volume for the target, then proceeding with remote target "\
                  "CRUD test"
            if self.crud_logical_drive(create_logical_drive_for_target_crud=True) == ValidationStatus.PASSED:
                chosen_logical_drive = self.created_logical_drive
                ld_created_for_crud = True
            else:
                cts_error("Creating a new logical volume for the target failed, skipping remote target tests")
                return ValidationStatus.BLOCKED

        targets = [link for link, resource in
                   self.discovery_container.get_resources(MetadataConstants.REMOTE_TARGET,
                                                          any_child_version=True,
                                                          constraints=[
                                                              from_collection(chosen_remote_target_collection)
                                                          ])]
        for target in targets:
            addresses = self.discovery_container[target].body["Addresses"]
            for address in addresses:
                try:
                    target_iqns.add(address["iSCSI"]["TargetIQN"])
                except KeyError:
                    pass

        if target_iqn in target_iqns:
            iqn_number = 0
            while target_iqn + str(iqn_number) in target_iqns:
                iqn_number += 1
            target_iqn = target_iqn + str(iqn_number)

        print "MESSAGE::Remote Target CRUD test will be performed on remote target collection %s, creating remote "\
              "target based on %s with first lowest consecutive \'cts.target:cts_test_target<number>\' target IQN "\
              "available: %s" % (chosen_remote_target_collection, chosen_logical_drive, target_iqn)

        self.post_remote_target_payload = dict(
            Addresses=[dict(
                iSCSI=dict(
                    TargetIQN=target_iqn,
                    TargetLUN=[dict(
                        LUN=1,
                        LogicalDrive={"@odata.id": urlparse(chosen_logical_drive).path}
                    )],
                )
            )],
            Initiator=[dict(
                iSCSI=dict(
                    InitiatorIQN="cts.initiator:initiator_cts_test"
                )
            )]
        )

        status = self._test_case_create_remote_target(chosen_remote_target_collection)
        if status == ValidationStatus.PASSED: # only perform other tests if creation was successful
            status = ValidationStatus.join_statuses(status, self._test_case_get_created_remote_target())
            status = ValidationStatus.join_statuses(status, self._test_case_logical_drive_has_link_to_created_target(
                chosen_logical_drive))
            status = ValidationStatus.join_statuses(status, self._test_case_update_created_remote_target())
            status = ValidationStatus.join_statuses(status, self._test_case_delete_created_remote_target())
            status = ValidationStatus.join_statuses(status, self._test_case_get_deleted_remote_target())

        if ld_created_for_crud:
            ld = self.created_logical_drive
            del_status = self._test_case_delete_created_logical_drive(ld)
            del_status = ValidationStatus.join_statuses(del_status, self._test_case_get_deleted_logical_drive(ld))
            if status == ValidationStatus.PASSED and del_status != status:
                cts_error("Remote target CRUD test passed, but the logical drive it created failed to delete properly")
                status = ValidationStatus.PASSED_WITH_WARNINGS

        return status
示例#6
0
class CRUDOperations(CtsTestScript):
    TIMEOUT = 600
    DESCRIPTION = """test script validating CRUD (create, read, update and delete) functionalities"""

    def run(self):
        test_name = "Storage Services CRUD test"
        print "MESSAGE::%s starting" % test_name

        print "TEST_CASE::API crawling"
        api_explorer = ApiExplorer(self.metadata_container, self.configuration)
        self.discovery_container, status = api_explorer.discover(MetadataConstants.SERVICE_ROOT_URI,
                                                            MetadataConstants.SERVICE_ROOT)
        print "STATUS::{status}".format(status=status)

        requirements = [
            Requirement(MetadataConstants.SERVICE_ROOT, min=1, max=1),
            Requirement(MetadataConstants.STORAGE_SERVICE, min=1),
            Requirement(MetadataConstants.LOGICAL_DRIVE, min=2)
        ]
        preconditions = Preconditions(self.metadata_container, requirements)
        status = preconditions.validate(self.discovery_container)

        self.api_caller = ApiCaller(self.configuration)

        status = ValidationStatus.join_statuses(status, self.crud_logical_drive())
        status = ValidationStatus.join_statuses(status, self.crud_remote_target())

        print "MESSAGE::%s overall status: %s" % (test_name, ColorPrinter.format_status(status))


    def _test_case_create_logical_drive(self, logical_drive_collection, payload=None):
        print "TEST_CASE::Create a logical drive"

        if not payload:
            payload = self.post_logical_drive_payload
        status, status_code, response_body, headers = self.api_caller.post_resource(
            logical_drive_collection, payload=payload)

        if not status:
            print "ERROR::Wrong status code of POST response: %d" % status_code
            self.set_status_failed()
            return ValidationStatus.FAILED

        try:
            self.created_logical_drive = headers["Location"]
            print "MESSAGE::Newly created logical drive url %s" % self.created_logical_drive
        except KeyError:
            print "ERROR::In header shall be provided location to created resource"
            self.set_status_failed()
            return ValidationStatus.FAILED

        print "MESSAGE::Logical drive created"
        self.set_status_passed()
        return ValidationStatus.PASSED

    def _test_case_get_created_logical_drive(self, logical_drive=None):
        print "TEST_CASE::Get the created logical drive"

        if not logical_drive:
            logical_drive = self.created_logical_drive
        status, status_code, response_body, _ = self.api_caller.get_resource(logical_drive)

        if not status:
            print "ERROR::Wrong status code of GET response: %s" % status_code
            print "ERROR::Response body:\n%s" % response_body
            self.set_status_failed()
            return ValidationStatus.FAILED
        else:
            api_resource = ApiResource(response_body, response_body["@odata.type"])
            self.discovery_container.add_resource(logical_drive, api_resource)
            print "MESSAGE::Comparing newly created logical drive to given payload"
            ignored_fields = ["/Name", "/Image"]  # Name setting not supported by PSME REST API, Image is optional

            print "MESSAGE::Comparing newly created logical drive to given payload"
            if not JsonComparator.compare_json_to_golden(response_body, self.expected_created_logical_drive_body,
                                                         ignore = ignored_fields):
                print "ERROR::Newly created logical drive's body incorrect"
                self.set_status_failed()
                print "MESSAGE::Proceeding with deleting the resource"
                return ValidationStatus.FAILED

        print "MESSAGE::Newly created logical drive's body correct"
        self.set_status_passed()
        return ValidationStatus.PASSED

    def _test_case_update_created_logical_drive(self):
        print "TEST_CASE::Update the created logical drive"

        patch_logical_drive_payload = dict(
            Bootable = not self.post_logical_drive_payload["Bootable"]
        )

        status, status_code, response_body, _ = self.api_caller.patch_resource(self.created_logical_drive,
                                                                               payload=patch_logical_drive_payload)

        if not status:
            print "ERROR::Wrong status code of PATCH reponse %s" % status_code
            print "ERROR::Response body:\n%s" % response_body
            self.set_status_failed()
            return ValidationStatus.FAILED

        status, status_code, response_body, _ = self.api_caller.get_resource(self.created_logical_drive)

        if not status:
            print "ERROR::Wrong status code of GET reponse %s" % status_code
            print "ERROR::Response body:\n%s" % response_body
            self.set_status_failed()
            return ValidationStatus.FAILED
        else:
            print "MESSAGE::Verifying whether the logical drive's update has taken place"
            if not JsonComparator.compare_json_to_golden(response_body, patch_logical_drive_payload):
                print "ERROR::Newly created logical drive could not be updated"
                self.set_status_failed()
                print "MESSAGE::Proceeding with deleting the resource"
                return ValidationStatus.FAILED

        print "MESSAGE::Newly created logical drive updated correctly"
        self.set_status_passed()
        return ValidationStatus.PASSED

    def _test_case_delete_created_logical_drive(self, logical_drive=None):
        print "TEST_CASE::Delete the created logical drive"

        if not logical_drive:
            logical_drive = self.created_logical_drive

        status, status_code, response_body, _ = self._delete_resource(logical_drive)

        if not status:
            self.set_status_failed()
            return ValidationStatus.FAILED

        self.discovery_container.remove_resource(logical_drive)
        print "MESSAGE::Newly created logical drive deleted"
        self.set_status_passed()
        return ValidationStatus.PASSED

    def _test_case_get_deleted_logical_drive(self, logical_drive=None):
        print "TEST_CASE::Get the deleted logical drive"

        if not logical_drive:
            logical_drive = self.created_logical_drive
        status, status_code, response_body, _ = self.api_caller.get_resource(logical_drive,
            acceptable_return_codes = [ReturnCodes.NOT_FOUND])

        if not status:
            print "ERROR::Wrong status code of GET response after deletion: %s" % status_code
            print "ERROR::Response body:\n%s" % response_body
            self.set_status_failed()
            return ValidationStatus.FAILED

        print "MESSAGE::Newly created logical drive not found (as intended)"
        self.set_status_passed()
        return ValidationStatus.PASSED


    def crud_logical_drive(self, called_from_crud_remote_target=None):
        """
        Test is trying to perform CRUD (create, read, update, delete) operations on a logical volume
        :type discovery_container: cts_core.discovery.discovery_container.DiscoveryContainer
        :type self.metadata_container: cts_core.metadata.self.metadata_container.MetadataContainer
        """

        status = ValidationStatus.BLOCKED

        lvm_found = False
        logical_volumes = dict()
        logical_volume_groups = dict()
        logical_drive_collections = dict()

        for resource_link, resource in self.discovery_container.iteritems():
            ok = True
            if self.metadata_container.entities[MetadataConstants.LOGICAL_DRIVE].compare_entities(resource.odata_type):
                print "DEBUG::found entity of type %s in %s" % (MetadataConstants.LOGICAL_DRIVE, resource_link)
                try:
                    if resource.body["Type"] == "LVM":
                        lvm_found = True
                        if resource.body["Mode"] == "LVG":
                            logical_volume_groups[resource_link] = resource
                        elif resource.body["Mode"] == "LV":
                            if "CapacityGiB" not in resource.body.keys():
                                print "DEBUG::%s drive does not have specified capacity" % resource_link
                                ok = False
                            if "Snapshot" in resource.body.keys() and resource.body["Snapshot"] == True:
                                print "DEBUG::%s cannot be used - drive is a snapshot volume" % resource_link
                                ok = False
                            if ok:
                                logical_volumes[resource_link] = resource
                    elif resource.body["Type"] == "CEPH":
                        if resource.body["Mode"] == "Pool":
                            logical_volume_groups[resource_link] = resource
                        elif resource.body["Mode"] == "RBD":
                            if "CapacityGiB" not in resource.body.keys():
                                print "DEBUG::%s drive does not have specified capacity" % resource_link
                                ok = False
                            if ok:
                                logical_volumes[resource_link] = resource
                except:
                    print "WARNING::Incorrect resource %s structure" % resource_link
            elif self.metadata_container.entities[MetadataConstants.LOGICAL_DRIVE_COLLECTION]\
                 .compare_entities(resource.odata_type):
                logical_drive_collections[resource_link] = resource

        if lvm_found and (not logical_volume_groups or not logical_volumes):
            print "ERROR::Insufficient resources available on API for test. Found %s logical volumes and %s logical " \
                  " volume groups" % (len(logical_volumes), len(logical_volume_groups))
            return ValidationStatus.BLOCKED

        # to speed things up, always look for the lowest capacity logical volume
        lowest_capacity = float('inf')
        chosen_logical_drive_collection, chosen_logical_volume, chosen_logical_volume_group = None, None, None
        inherited_bootable = None

        if not logical_volume_groups:
            if lvm_found:
                print "ERROR::No LVM volume group found, aborting"
            else:
                print "ERROR::No CEPH Pool device found, aborting"
            return ValidationStatus.BLOCKED

        for lvg_link, lvg in logical_volume_groups.iteritems():
            # finding a collection matching this volume group
            possible_collections = [collection for collection in logical_drive_collections.keys() if collection in lvg_link]
            if not possible_collections:
                continue
            try:
                for lv in lvg.body["Links"]["LogicalDrives"]: # iterating LVs that have this LVG as a parent
                    try:
                        lv_link = lv["@odata.id"]
                        lv_type = logical_volumes[lv_link].body["Type"]
                        lv_mode = logical_volumes[lv_link].body["Mode"]
                        lv_capacity = logical_volumes[lv_link].body["CapacityGiB"]
                        lv_bootable = logical_volumes[lv_link].body["Bootable"]
                        if lv_capacity < lowest_capacity:
                            lowest_capacity = lv_capacity
                            inherited_bootable = lv_bootable
                            chosen_logical_volume = lv_link
                            chosen_logical_volume_type = lv_type
                            chosen_logical_volume_mode = lv_mode
                            chosen_logical_volume_group = lvg_link
                            chosen_logical_drive_collection = possible_collections[0]
                    except KeyError:
                        pass
            except KeyError:
                pass

        self.post_logical_drive_payload = dict(
            Name="LVM Logical Drive created by CTS",
            Type=chosen_logical_volume_type,
            Mode=chosen_logical_volume_mode,
            Protected=False,
            CapacityGiB=self.discovery_container[chosen_logical_volume].body["CapacityGiB"],
            Bootable=inherited_bootable if inherited_bootable else False,
            Snapshot=True,
            Links=dict(
                LogicalDrives=[{"@odata.id": chosen_logical_volume_group}],
                MasterDrive={"@odata.id": chosen_logical_volume}
            )
        )

        # Creating the first RBD if no other was found
        if not chosen_logical_volume:
            if lvm_found:
                print "ERROR::Could not find a (logical volume, logical volume group) pair suitable for the test"
                return status
            else:
                self.post_logical_drive_payload = dict(
                    Name="CEPH Logical Drive created by CTS",
                    Type="CEPH",
                    Mode="RBD",
                    Protected=False,
                    CapacityGiB=self.discovery_container[chosen_logical_volume_group].body["CapacityGiB"]/2,
                    Bootable=False,
                    Snapshot=False,
                    Links=dict(
                        LogicalDrives=[{"@odata.id": chosen_logical_volume_group}],
                        MasterDrive=None
                    )
                )


        print "MESSAGE::Logical drive CRUD test will be performed on logical drive collection %s, creating logical volume " \
              "based on %s on group %s" % (chosen_logical_drive_collection, chosen_logical_volume, chosen_logical_volume_group)

        self.expected_created_logical_drive_body = deepcopy(self.post_logical_drive_payload)
        # The logical drive in LogicalDrives link is really the volume group and is going to be seen under
        # UsedBy on REST
        self.expected_created_logical_drive_body["Links"]["UsedBy"] = \
            self.expected_created_logical_drive_body["Links"]["LogicalDrives"]
        self.expected_created_logical_drive_body["Links"]["LogicalDrives"] = []

        status = self._test_case_create_logical_drive(chosen_logical_drive_collection)
        if status == ValidationStatus.PASSED: # only perform other tests if creation was successful
            status = ValidationStatus.join_statuses(status, self._test_case_get_created_logical_drive())
            if called_from_crud_remote_target:
                status = ValidationStatus.join_statuses(status,
                         self.crud_remote_target(called_from_crud_logical_drive=True))
            else:
                status = ValidationStatus.join_statuses(status, self._test_case_update_created_logical_drive())
            status = ValidationStatus.join_statuses(status, self._test_case_delete_created_logical_drive())
            status = ValidationStatus.join_statuses(status, self._test_case_get_deleted_logical_drive())

        return status


    def _test_case_create_remote_target(self, remote_target_collection):
        print "TEST_CASE::Create a remote target"

        status, status_code, response_body, headers = self.api_caller.post_resource(
            remote_target_collection, payload=self.post_remote_target_payload)

        if not status:
            print "ERROR::Wrong status code of POST response: %d" % status_code
            self.set_status_failed()
            return ValidationStatus.FAILED

        try:
            self.created_remote_target = headers["Location"]
            print "MESSAGE::Newly created remote target url %s" % self.created_remote_target
        except KeyError:
            print "ERROR::In header shall be provided location to created resource"
            self.set_status_failed()
            return ValidationStatus.FAILED

        print "MESSAGE::Remote target created"
        self.set_status_passed()
        return ValidationStatus.PASSED

    def _test_case_get_created_remote_target(self):
        print "TEST_CASE::Get the created remote target"

        status, status_code, response_body, _ = self.api_caller.get_resource(self.created_remote_target)

        if not status:
            print "ERROR::Wrong status code of GET response: %s" % status_code
            print "ERROR::Response body:\n%s" % response_body
            self.set_status_failed()
            return ValidationStatus.FAILED
        else:
            print "MESSAGE::Comparing newly created remote target to given payload"
            if not JsonComparator.compare_json_to_golden(response_body, self.post_remote_target_payload):
                print "ERROR::Newly created remote target's body incorrect"
                self.set_status_failed()
                print "MESSAGE::Proceeding with deleting the resource"
                return ValidationStatus.FAILED

        print "MESSAGE::Newly created remote target's correct"
        self.set_status_passed()
        return ValidationStatus.PASSED

    def _test_case_logical_volume_has_link_to_created_target(self, logical_volume):
        print "TEST_CASE::Check for a link to the created target in logical volume's body"

        status, status_code, response_body, _ = self.api_caller.get_resource(logical_volume)

        if not status:
            print "ERROR::Wrong status code of GET response: %s" % status_code
            print "ERROR::Response body:\n%s" % response_body
            self.set_status_failed()
            return ValidationStatus.FAILED
        else:
            try:
                target = urlparse(self.created_remote_target).path
                if not {"@odata.id": target} in response_body["Links"]["Targets"]\
                   and not {"@odata.id": target + "/"} in response_body["Links"]["Targets"]:
                    print "ERROR::No link to the created target in the logical volume's body"
                    self.set_status_failed()
                    return ValidationStatus.FAILED
            except KeyError:
                    print "ERROR::No Links/Targets array present in the logical volume's body"
                    self.set_status_failed()
                    return ValidationStatus.FAILED

        print "MESSAGE::A link from the logical volume to the target is present"
        self.set_status_passed()
        return ValidationStatus.PASSED

    def _test_case_update_created_remote_target(self):
        print "TEST_CASE::Update the created remote target"

        patch_remote_target_payload = dict(
            Initiator=[dict(
                iSCSI=dict(
                    InitiatorIQN="cts.initiator:patched_cts_initiator"
                )
            )]
        )

        status, status_code, response_body, _ = self.api_caller.patch_resource(self.created_remote_target,
                                                                               payload=patch_remote_target_payload)

        if not status:
            print "ERROR::Wrong status code of PATCH reponse %s" % status_code
            print "ERROR::Response body:\n%s" % response_body
            self.set_status_failed()
            return ValidationStatus.FAILED

        status, status_code, response_body, _ = self.api_caller.get_resource(self.created_remote_target)

        if not status:
            print "ERROR::Wrong status code of GET reponse %s" % status_code
            print "ERROR::Response body:\n%s" % response_body
            self.set_status_failed()
            return ValidationStatus.FAILED
        else:
            print "MESSAGE::Verifying whether the remote target's update has taken place"
            if not JsonComparator.compare_json_to_golden(response_body, patch_remote_target_payload):
                print "ERROR::Newly created remote target could not be updated"
                self.set_status_failed()
                print "MESSAGE::Proceeding with deleting the resource"
                return ValidationStatus.FAILED

        print "MESSAGE::Newly created remote target updated correctly"
        self.set_status_passed()
        return ValidationStatus.PASSED

    def _test_case_delete_created_remote_target(self):
        print "TEST_CASE::Delete the created remote target"

        status, status_code, response_body, _ = self._delete_resource(self.created_remote_target)

        if not status:
            self.set_status_failed()
            return ValidationStatus.FAILED

        print "MESSAGE::Newly created remote target deleted"
        self.set_status_passed()
        return ValidationStatus.PASSED

    def _test_case_get_deleted_remote_target(self):
        print "TEST_CASE::Get the deleted remote target"

        status, status_code, response_body, _ = self.api_caller.get_resource(self.created_remote_target,
            acceptable_return_codes = [ReturnCodes.NOT_FOUND])

        if not status:
            print "ERROR::Wrong status code of GET response after deletion: %s" % status_code
            self.set_status_failed()
            return ValidationStatus.FAILED

        print "MESSAGE::Newly created remote target not found (as intended)"
        self.set_status_passed()
        return ValidationStatus.PASSED

    def _delete_resource(self, resource_link):
        status, status_code, response_body, headers = self.api_caller.delete_resource(resource_link)

        if not status:
            print "ERROR::Wrong status code of DELETE reponse: %s" % status_code
            print "ERROR::Response body:\n%s" % response_body

        return status, status_code, response_body, headers

    def crud_remote_target(self, called_from_crud_logical_drive=None):
        """
        Test is checking for rsa compute blade presence
        :type discovery_container: cts_core.discovery.discovery_container.DiscoveryContainer
        :type self.metadata_container: cts_core.metadata.self.metadata_container.MetadataContainer
        """
        target_iqn = "cts.target:cts_test_target"
        target_iqns = set()

        logical_volumes = dict()
        remote_target_collections = dict()

        for resource_link, resource in self.discovery_container.iteritems():
            if self.metadata_container.entities[MetadataConstants.LOGICAL_DRIVE].compare_entities(resource.odata_type):
                print "DEBUG::found entity of type %s in %s" % (MetadataConstants.LOGICAL_DRIVE, resource_link)
                try:
                    # looking for unprotected LVs with no targets associated with it
                    if ((resource.body["Mode"] == "LV" or resource.body["Mode"] == "RBD")
                        and not len(resource.body["Links"]["Targets"]) and not resource.body["Protected"]):
                        logical_volumes[resource_link] = resource
                except:
                    print "WARNING::Incorrect resource %s structure" % resource_link
            elif self.metadata_container.entities[MetadataConstants.REMOTE_TARGET_COLLECTION].compare_entities(resource.odata_type):
                remote_target_collections[resource_link] = resource
            elif self.metadata_container.entities[MetadataConstants.REMOTE_TARGET].compare_entities(
                    resource.odata_type):
                try:
                    for address in resource.body["Addresses"]:
                        target_iqns.add(address["iSCSI"]["TargetIQN"])
                except KeyError:
                    pass

        if target_iqn in target_iqns:
            iqn_number = 0
            while target_iqn + str(iqn_number) in target_iqns:
                iqn_number += 1
            target_iqn = target_iqn + str(iqn_number)

        if not logical_volumes:
            print "WARNING::Insufficient resources available on API for test. Could not find an unprotected logical volume with"\
                  " no targets attached"
            if called_from_crud_logical_drive:
                print "ERROR::Creating a new logical volume for the target failed, skipping remote target tests"
                return ValidationStatus.BLOCKED
            else:
                print "MESSAGE::Trying to create a new logical volume for the target, then proceeding with remote target CRUD test"
                return self.crud_logical_drive(called_from_crud_remote_target=True)

        chosen_logical_volume = logical_volumes.keys()[0]
        chosen_remote_target_collection = remote_target_collections.keys()[0]

        print "MESSAGE::Remote Target CRUD test will be performed on remote target collection %s, creating remote target " \
              "based on %s" % (chosen_remote_target_collection, chosen_logical_volume)

        self.post_remote_target_payload = dict(
            Addresses=[dict(
                iSCSI=dict(
                    TargetIQN=target_iqn,
                    TargetLUN=[dict(
                        LUN=1,
                        LogicalDrive={"@odata.id": chosen_logical_volume}
                    )],
                )
            )],
            Initiator=[dict(
                iSCSI=dict(
                    InitiatorIQN="cts.initiator:initiator_cts_test"
                )
            )]
        )

        status = self._test_case_create_remote_target(chosen_remote_target_collection)
        if status == ValidationStatus.PASSED: # only perform other tests if creation was successful
            status = ValidationStatus.join_statuses(status, self._test_case_get_created_remote_target())
            status = ValidationStatus.join_statuses(status, self._test_case_logical_volume_has_link_to_created_target(
                chosen_logical_volume))
            status = ValidationStatus.join_statuses(status, self._test_case_update_created_remote_target())
            status = ValidationStatus.join_statuses(status, self._test_case_delete_created_remote_target())
            status = ValidationStatus.join_statuses(status, self._test_case_get_deleted_remote_target())

        return status
示例#7
0
class ApiCallerCallsUnitTest(unittest.TestCase):
    def setUp(self):
        configuration = Configuration(**dict(ApiEndpoint=API_ENDPOINT))
        self.api_caller = ApiCaller(configuration)

    @unittest.skip("Temporary change related to bug in Discovery mechanism")
    def test_get_on_empty_resource(self):
        with mock.patch('requests.get') as requests_get_mock:
            response = Mock()
            response.status_code = 200
            response.headers = {}
            response.text = ""
            requests_get_mock.return_value = response
            with StdoutCapture() as output:
                self.api_caller.get_resource("/resource", DiscoveryContainer())
            self.assertIn(
                "ERROR::url=/resource Get failed. Status code: None;",
                output.raw)

    def test_empty_service_root(self):
        with mock.patch('requests.get') as requests_get_mock:
            requests_get_mock.side_effect = requests.ConnectionError()
            with StdoutCapture() as output:
                self.api_caller.get_resource("/redfish/v1",
                                             DiscoveryContainer())
            self.assertIn(
                "ERROR::Get url=http://1.2.3.4:567/redfish/v1 Error <class 'requests.exceptions.ConnectionError'>:;",
                output.raw)

    @unittest.skip("Temporary change related to bug in Discovery mechanism")
    def test_incorrect_status_code(self):
        with mock.patch('requests.get') as requests_get_mock:
            response = Mock()
            response.status_code = 500
            response.headers = {}
            response.text = "{}"
            requests_get_mock.return_value = response
            with StdoutCapture() as output:
                self.api_caller.get_resource("/resource", DiscoveryContainer())
            self.assertIn("ERROR::url=/resource Get failed. Status code: 500;",
                          output.raw)

    def test_request_exception(self):
        with mock.patch('requests.get') as requests_get_mock:
            requests_get_mock.side_effect = requests.RequestException()
            with StdoutCapture() as output:
                self.api_caller.get_resource("/resource", DiscoveryContainer())
            self.assertIn(
                "ERROR::Get url=http://1.2.3.4:567/resource Error <class 'requests.exceptions.RequestException'>:;",
                output.raw)

    def test_connection_error_exception(self):
        with mock.patch('requests.get') as requests_get_mock:
            requests_get_mock.side_effect = requests.ConnectionError()
            with StdoutCapture() as output:
                self.api_caller.get_resource("/resource", DiscoveryContainer())
            self.assertIn(
                "ERROR::Get url=http://1.2.3.4:567/resource Error <class 'requests.exceptions.ConnectionError'>:;",
                output.raw)

    def test_http_error_exception(self):
        with mock.patch('requests.get') as requests_get_mock:
            requests_get_mock.side_effect = requests.HTTPError()
            with StdoutCapture() as output:
                self.api_caller.get_resource("/resource", DiscoveryContainer())
            self.assertIn(
                "ERROR::Get url=http://1.2.3.4:567/resource Error <class 'requests.exceptions.HTTPError'>:",
                output.raw)

    def test_url_required_exception(self):
        with mock.patch('requests.get') as requests_get_mock:
            requests_get_mock.side_effect = requests.URLRequired()
            with StdoutCapture() as output:
                self.api_caller.get_resource("/resource", DiscoveryContainer())
            self.assertIn(
                "ERROR::Get url=http://1.2.3.4:567/resource Error <class "
                "'requests.exceptions.URLRequired'>:", output.raw)

    def test_to_many_redirects_exception(self):
        with mock.patch('requests.get') as requests_get_mock:
            requests_get_mock.side_effect = requests.TooManyRedirects()
            with StdoutCapture() as output:
                self.api_caller.get_resource("/resource", DiscoveryContainer())
            self.assertIn(
                "ERROR::Get url=http://1.2.3.4:567/resource Error <class 'requests.exceptions.TooManyRedirects'>:;",
                output.raw)

    def test_timeout_exception(self):
        with mock.patch('requests.get') as requests_get_mock:
            requests_get_mock.side_effect = requests.Timeout()
            with StdoutCapture() as output:
                self.api_caller.get_resource("/resource", DiscoveryContainer())
            self.assertIn(
                "ERROR::Get url=http://1.2.3.4:567/resource Error <class 'requests.exceptions.Timeout'>:;",
                output.raw)

    def test_incorrect_body(self):
        with mock.patch('requests.get') as requests_get_mock:
            response = Mock()
            response.status_code = 200
            response.headers = {}
            response.text = "not a json"
            requests_get_mock.return_value = response

            with StdoutCapture() as output:
                self.api_caller.get_resource("/resource", DiscoveryContainer())
            self.assertIn("ERROR", output.raw)

    def test_parse_big_ordered_tree_response(self):
        from collections import OrderedDict
        response_payload = OrderedDict(
            [(u'@odata.context', u'/redfish/v1/$metadata#ServiceRoot.ServiceRoot'), (u'@odata.etag', u'W/"1557488360"'),
             (u'@odata.id', \
              u'/redfish/v1/'), (u'@odata.type', u'#ServiceRoot.v1_3_1.ServiceRoot'),
             (u'AccountService', OrderedDict([(u'@odata.id', u'/redfish/v1/AccountService')])),
             (u'Chassis', OrderedDict([(u'@odata.id', u'/redfish/v1/Chassis')])), (u'Description', u'The service root \
                                                                                   for all Redfish requests on this host \
             '              ), (u'EventService', OrderedDict([(u' @ odata.id', u' / redfish / v1 / EventService')])),
             (u'Id', u'RootService'), (
                 u'Links', OrderedDict(
                     [(u'Oem', OrderedDict([(u'Ami', OrderedDict([(u'@odata.id', u'/redfish/v1/configurations')]))])),
                      (u'Sessions', OrderedDict([(u'@odata.id', u'/redfish/v1/SessionService/Sessions')]))])), (
                 u'Managers', OrderedDict([(u'@odata.id',
                                            u'/redfish/v1/Managers')])), (u'Name', u'PSME Service Root'), (
                 u'Oem', OrderedDict([(u'Ami', OrderedDict([(u'Chassislocation',
                                                             OrderedDict(
                                                                 [(u'@odata.id', u'/redfish/v1/Chassislocation')])),
                                                            (u'Configurations', OrderedDict([(u'@odata.id',
                                                                                              u'/redfish/v1/configurations')])),
                                                            (u'PsmeVersion', u'2.4.181218.tb1')])),
                                      (u'Intel_RackScale', OrderedDict([(u'@odata.type',
                                                                         u'#Intel.Oem.ServiceRoot'),
                                                                        (u'ApiVersion', u'2.4.0'),
                                                                        (
                                                                        u'TelemetryService', OrderedDict([(u'@odata.id',
                                                                                                           u'/redfish/v1/TelemetryService')]))]))])),
             (u'Product', u'AMI Redfish Server'), (u'ProtocolFeaturesSupported',
                                                      OrderedDict([(u'ExpandQuery', OrderedDict(
                                                          [(u'ExpandAll', True), (u'Levels', True), (u'Links', True),
                                                           (u'MaxLevels', 5), (u'NoLinks',
                                                                               True)])), (u'FilterQuery', True),
                                                                   (u'SelectQuery', True)])), (
                 u'RedfishVersion', u'1.5.0'), (u'Registries', OrderedDict([(u'@odata.id',
                                                                             u'/redfish/v1/Registries')])), (
                 u'SessionService', OrderedDict([(u'@odata.id', u'/redfish/v1/SessionService')])), (u'Systems',
                                                                                                    OrderedDict([(
                                                                                                        u'@odata.id',
                                                                                                        u'/redfish/v1/Systems')])),
             (u'Tasks', OrderedDict([(u'@odata.id', u'/redfish/v1/TaskService')])), (u'UUID',
                                                                                        u'ffffffff-ffff-ffff-ffff-ffffffffffff'),
             (u'UpdateService', OrderedDict([(u'@odata.id', u'/redfish/v1/UpdateService')]))])

        with mock.patch('requests.get') as requests_get_mock:
            response = Mock()
            response.status_code = 200
            response.headers = {}
            response.text = response_payload
            requests_get_mock.return_value = response

            with StdoutCapture() as output:
                self.api_caller.get_resource("/resource", DiscoveryContainer())
            self.assertIsNot("ERROR", output.raw)

    def test_no_content(self):
        with mock.patch('requests.get') as requests_get_mock:
            response = Mock()
            response.status_code = 204
            response.headers = {}
            response.text = None
            requests_get_mock.return_value = response

            link, status, status_code, response_body, headers = \
                self.api_caller.get_resource("/resource", DiscoveryContainer(), acceptable_return_codes = [204])
            self.assertTrue(status)
            self.assertEqual(status_code, 204)
            self.assertEqual(response_body, dict())
            response.json.assert_not_called()

    def test_async_create_without_location(self):
        with mock.patch('requests.post') as requests_post_mock:
            response = Mock()
            response.status_code = ReturnCodes.ACCEPTED
            response.headers = {}
            response.text = None
            requests_post_mock.return_value = response

            with mock.patch('requests.get') as requests_get_mock:
                response = Mock()
                response.status_code = 204
                response.headers = {}
                response.text = None
                requests_get_mock.return_value = response

                with StdoutCapture() as output:
                    status, status_code, response_body, headers = self.api_caller.post_resource(
                        'odata_id', DiscoveryContainer(), payload={})

            self.assertIn('Location header not found', '\n'.join(output))

    def test_unexpected_async_post_response(self):
        with mock.patch('requests.post') as requests_get_mock:
            response = Mock()
            response.status_code = ReturnCodes.ACCEPTED
            response.headers = {}
            response.text = "{}"
            requests_get_mock.return_value = response

            with self.assertRaises(AsyncOperation):
                self.api_caller.post_resource('odata_id',
                                              DiscoveryContainer(),
                                              payload={},
                                              wait_if_async=False)

    def test_async_post_response(self):
        with mock.patch('requests.post') as requests_post_mock:
            not_done = Mock()
            not_done.status_code = ReturnCodes.ACCEPTED
            not_done.headers = {'Location': 'location'}
            not_done.text = None
            requests_post_mock.return_value = not_done

            with mock.patch('requests.get') as requests_get_mock:
                done = Mock()
                done.status_code = ReturnCodes.OK
                done.headers = {'Location': 'location'}
                done.text = "{ \"done\": true }"

                requests_get_mock.side_effect = [not_done, not_done, done]

                with StdoutCapture() as output:
                    status, status_code, response_body, headers = self.api_caller.post_resource(
                        'odata_id', DiscoveryContainer(), payload={})
                    self.assertTrue(response_body['done'])

    @unittest.skip("Temporary change related to bug in Discovery mechanism")
    def test_resource_in_discovery_container_after_get_patch_delete(self):
        with mock.patch('requests.get') as requests_get_mock:
            resource = {"@odata.id": "odata.id", "something": "irrelevant"}
            get_response = Mock()
            get_response.status_code = ReturnCodes.OK
            get_response.headers = {}
            get_response.text = json.dumps(resource)
            requests_get_mock.return_value = get_response
            discovery_container = DiscoveryContainer()
            self.api_caller.get_resource("/resource", discovery_container)
            self.assertEqual(
                discovery_container["http://{API_ENDPOINT}/resource".format(
                    API_ENDPOINT=API_ENDPOINT)].body, resource)

            patched_resource = {
                "@odata.id": "odata.id",
                "something": "relevant"
            }
            get_response.text = json.dumps(patched_resource)

            with mock.patch('requests.patch') as requests_patch_mock:
                patch_response = Mock()
                patch_response.status_code = ReturnCodes.OK
                patch_response.headers = {}
                patch_response.text = "{}"
                requests_patch_mock.return_value = patch_response
                _, _, _, _ = self.api_caller.patch_resource(
                    "/resource", discovery_container)
                self.assertEqual(
                    discovery_container[
                        "http://{API_ENDPOINT}/resource".format(
                            API_ENDPOINT=API_ENDPOINT)].body, patched_resource)

                with mock.patch('requests.delete') as requests_delete_mock:
                    delete_response = Mock()
                    delete_response.status_code = ReturnCodes.NO_CONTENT
                    delete_response.headers = {}
                    delete_response.text = ""
                    requests_delete_mock.return_value = delete_response
                    _, _, _, _ = self.api_caller.delete_resource(
                        "/resource", discovery_container)
                    self.assertNotIn("/resource", discovery_container)

    def test_get_xml(self):
        with mock.patch('requests.get') as get:
            response = Mock()
            response.status_code = ReturnCodes.OK
            response.headers = {}
            response.text = "<xml></xml>"
            get.return_value = response

            link, status, status_code, response_body, headers = self.api_caller.get_xml(
                "uri")
        self.assertEqual("<xml></xml>", response_body)