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
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)
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
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
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
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
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)