예제 #1
0
class LinksFactoryUnitTest(unittest.TestCase):
    def setUp(self):
        self.links_factory = LinksFactory(ENDPOINT, ssl=True)

    def test_relative_with_absolute_path(self):
        self.assertEqual(
            "https://{enpoint}{odata_id}".format(
                enpoint=ENDPOINT,
                odata_id=RELATIVE_WITH_ABSOLUTE_PATH_ODATA_ID),
            self.links_factory.get_resource_link(
                RELATIVE_WITH_ABSOLUTE_PATH_ODATA_ID).link)

    def test_relative(self):
        self.assertEqual(
            "https://" + RELATIVE_ODATA_ID_WITH_AUTHORITY[2:],
            self.links_factory.get_resource_link(
                RELATIVE_ODATA_ID_WITH_AUTHORITY).link)
        self.assertEqual(
            "https://" + RELATIVE_ODATA_ID_WITH_AUTHORITY_WITH_PORT[2:],
            self.links_factory.get_resource_link(
                RELATIVE_ODATA_ID_WITH_AUTHORITY_WITH_PORT).link)

    def test_absolute(self):
        self.assertEqual(
            ABSOLUTE_ODATA_ID,
            self.links_factory.get_resource_link(ABSOLUTE_ODATA_ID).link)
        self.assertEqual(
            ABSOLUTE_ODATA_ID_WITH_TRAILING_SLASH[:-1],
            self.links_factory.get_resource_link(
                ABSOLUTE_ODATA_ID_WITH_TRAILING_SLASH).link)

    def test_with_override(self):
        self.assertEqual(
            "https://{enpoint}{odata_id}".format(
                enpoint=OVERRIDE,
                odata_id=RELATIVE_WITH_ABSOLUTE_PATH_ODATA_ID),
            self.links_factory.get_resource_link(
                RELATIVE_WITH_ABSOLUTE_PATH_ODATA_ID,
                api_endpoint_override=OVERRIDE).link)

        self.assertEqual(
            "https://" + RELATIVE_ODATA_ID_WITH_AUTHORITY[2:],
            self.links_factory.get_resource_link(
                RELATIVE_ODATA_ID_WITH_AUTHORITY,
                api_endpoint_override=OVERRIDE).link)
        self.assertEqual(
            "https://" + RELATIVE_ODATA_ID_WITH_AUTHORITY_WITH_PORT[2:],
            self.links_factory.get_resource_link(
                RELATIVE_ODATA_ID_WITH_AUTHORITY_WITH_PORT,
                api_endpoint_override=OVERRIDE).link)

        self.assertEqual(
            ABSOLUTE_ODATA_ID,
            self.links_factory.get_resource_link(
                ABSOLUTE_ODATA_ID, api_endpoint_override=OVERRIDE).link)
        self.assertEqual(
            ABSOLUTE_ODATA_ID_WITH_TRAILING_SLASH[:-1],
            self.links_factory.get_resource_link(
                ABSOLUTE_ODATA_ID_WITH_TRAILING_SLASH,
                api_endpoint_override=OVERRIDE).link)
예제 #2
0
class LinksFactoryUnitTest(unittest.TestCase):
    def setUp(self):
        self.links_factory = LinksFactory(ENDPOINT, ssl=True)

    def test_relative_with_absolute_path(self):
        self.assertEqual("https://{enpoint}{odata_id}".format(enpoint=ENDPOINT,
                                                              odata_id=RELATIVE_WITH_ABSOLUTE_PATH_ODATA_ID),
                         self.links_factory.get_resource_link(
                             RELATIVE_WITH_ABSOLUTE_PATH_ODATA_ID).link)

    def test_relative(self):
        self.assertEqual("https://" + RELATIVE_ODATA_ID_WITH_AUTHORITY[2:],
                         self.links_factory.get_resource_link(
                             RELATIVE_ODATA_ID_WITH_AUTHORITY).link)
        self.assertEqual("https://" + RELATIVE_ODATA_ID_WITH_AUTHORITY_WITH_PORT[2:],
                         self.links_factory.get_resource_link(
                             RELATIVE_ODATA_ID_WITH_AUTHORITY_WITH_PORT).link)

    def test_absolute(self):
        self.assertEqual(ABSOLUTE_ODATA_ID, self.links_factory.get_resource_link(
            ABSOLUTE_ODATA_ID).link)
        self.assertEqual(ABSOLUTE_ODATA_ID_WITH_TRAILING_SLASH[:-1],
                         self.links_factory.get_resource_link(
                             ABSOLUTE_ODATA_ID_WITH_TRAILING_SLASH).link)

    def test_with_override(self):
        self.assertEqual("https://{enpoint}{odata_id}".format(enpoint=OVERRIDE,
                                                              odata_id=RELATIVE_WITH_ABSOLUTE_PATH_ODATA_ID),
                         self.links_factory.get_resource_link(
                             RELATIVE_WITH_ABSOLUTE_PATH_ODATA_ID,
                             api_endpoint_override=OVERRIDE).link)

        self.assertEqual("https://" + RELATIVE_ODATA_ID_WITH_AUTHORITY[2:],
                         self.links_factory.get_resource_link(
                             RELATIVE_ODATA_ID_WITH_AUTHORITY,
                             api_endpoint_override=OVERRIDE).link)
        self.assertEqual("https://" + RELATIVE_ODATA_ID_WITH_AUTHORITY_WITH_PORT[2:],
                         self.links_factory.get_resource_link(
                             RELATIVE_ODATA_ID_WITH_AUTHORITY_WITH_PORT,
                             api_endpoint_override=OVERRIDE).link)

        self.assertEqual(ABSOLUTE_ODATA_ID, self.links_factory.get_resource_link(
            ABSOLUTE_ODATA_ID,
            api_endpoint_override=OVERRIDE).link)
        self.assertEqual(ABSOLUTE_ODATA_ID_WITH_TRAILING_SLASH[:-1],
                         self.links_factory.get_resource_link(
                             ABSOLUTE_ODATA_ID_WITH_TRAILING_SLASH,
                             api_endpoint_override=OVERRIDE).link)
예제 #3
0
    def __init__(self, configuration):
        try:
            ssl = strtobool(configuration.UseSSL)
        except ValueNotFound:
            ssl = False

        self._links_factory = LinksFactory(configuration.ApiEndpoint, ssl=ssl)
        self._config_property_reader = configuration
        try:
            self.script_execution_id = ScriptDAO.get_last_script_execution_id()
            if self.script_execution_id != None:
                script_execution = ScriptDAO.get_script_execution_details(self.script_execution_id)
                if getppid() != script_execution.pid:
                    # this is probably replay or running test script without framework
                    # do not register requests
                    self.script_execution_id = None
        except:
            self.script_execution_id = None

        self.request_registration = self.script_execution_id is not None
예제 #4
0
    def __init__(self, configuration):
        try:
            ssl = strtobool(configuration.UseSSL)
        except ValueNotFound:
            ssl = False

        self._links_factory = LinksFactory(configuration.ApiEndpoint, ssl=ssl)
        self._config_property_reader = configuration
        try:
            self.script_execution_id = ScriptDAO.get_last_script_execution_id()
            if self.script_execution_id != None:
                script_execution = ScriptDAO.get_script_execution_details(self.script_execution_id)
                if getppid() != script_execution.pid:
                    # this is probably replay or running test script without framework
                    # do not register requests
                    self.script_execution_id = None
        except:
            self.script_execution_id = None

        self.request_registration = self.script_execution_id is not None
예제 #5
0
 def __init__(self, config_property_reader):
     self._links_factory = LinksFactory(config_property_reader.ApiEndpoint)
     self._config_property_reader = config_property_reader
예제 #6
0
class ApiCaller:
    def __init__(self, config_property_reader):
        self._links_factory = LinksFactory(config_property_reader.ApiEndpoint)
        self._config_property_reader = config_property_reader

    def perform_call(self,
                     odata_id,
                     http_method=None,
                     acceptable_return_codes=None,
                     payload=None):
        if not acceptable_return_codes:
            acceptable_return_codes = [ReturnCodes.OK]

        if not http_method:
            http_method = HttpMethods.GET

        resource = self._links_factory.get_resource_link(odata_id)

        try:
            url, kwargs = self.build_request_params(resource, payload)
            requests_method = {
                HttpMethods.GET: requests.get,
                HttpMethods.PATCH: requests.patch
            }[http_method]

            print "DEBUG::\n%s %s\n\trequest parameters {\n\t\t%s\n\t}" % (
                http_method, url, "\n\t\t".join([
                    "%s: %s" % (str(key), str(value))
                    for key, value in kwargs.iteritems()
                ]))

            try:
                # Try to suppress user warnings
                urllib3.disable_warnings()
            except:
                pass

            response = requests_method(url, **kwargs)
            if response.status_code in [
                    ReturnCodes.UNAUTHORIZED, ReturnCodes.FORBIDDEN
            ] and response.status_code not in acceptable_return_codes:
                raise TestCaseFatalErrorFlag(
                    "API refuses provided authentication")

            status = RequestStatus.SUCCESS if response.status_code in acceptable_return_codes else RequestStatus.FAILED

            status_code, headers = response.status_code, response.headers

            if response.status_code in [
                    ReturnCodes.NO_CONTENT, ReturnCodes.CREATED
            ]:
                response_body = dict()
            else:
                response_body = response.json()

            pretty_response = json.dumps(response_body, indent=4).replace(
                "\n", "\n\t\t"
            ) if response.status_code != ReturnCodes.NO_CONTENT else "No content"

            print "RAW::\n\tresponse\n\t\t%s\n" % (pretty_response)
            print "RAW::\n\tSTATUS_CODE: %d, ACCEPTABLE: %s\n" % (
                response.status_code, str(acceptable_return_codes))
            if RequestStatus.SUCCESS != status:
                print "RAW::\n\t[WARNING] - %s %s\n" % (http_method, url)

        except requests.RequestException as err:
            raise TestCaseFatalErrorFlag(
                "Error: %s occurred when accessing resource %s" %
                (err.message, odata_id))
        except JSONDecodeError:
            raise TestCaseFatalErrorFlag(
                "Unable to parse resource %s to json" % odata_id)
        except Exception as err:
            raise TestCaseFatalErrorFlag(
                "Unknown exception %s while accessing resource %s" %
                (err.message, odata_id))

        return status, status_code, response_body, headers

    def get_resource(self, odata_id):
        return self.perform_call(odata_id)

    def build_request_params(self, resource, payload):
        kwargs = dict(verify=False,
                      headers={"Content-Type": "application/json"})
        try:
            kwargs["cert"] = (self._config_property_reader.CertificateCertFile,
                              self._config_property_reader.CertificateKeyFile)
        except ValueNotFound:
            #not using certificate authorization
            pass

        try:
            kwargs["auth"] = (self._config_property_reader.User,
                              self._config_property_reader.Password)
        except ValueNotFound:
            pass

        try:
            if strtobool(self._config_property_reader.UseSSL):
                url = HTTPS + resource
            else:
                url = HTTP + resource
        except ValueNotFound:
            url = HTTP + resource

        if payload:
            kwargs["data"] = json.dumps(payload)

        return url, kwargs

    def read_certificate(self, certificate_file):
        try:
            with open(certificate_file, "r") as f:
                return f.read()
        except Exception:
            return None
예제 #7
0
class ApiCaller:
    DEFAULT_RETURN_CODES = {
        HttpMethods.PATCH: [ReturnCodes.NO_CONTENT, ReturnCodes.ACCEPTED, ReturnCodes.OK],
        HttpMethods.POST: [ReturnCodes.CREATED, ReturnCodes.ACCEPTED],
        HttpMethods.DELETE: [ReturnCodes.NO_CONTENT, ReturnCodes.ACCEPTED],
        HttpMethods.GET: [ReturnCodes.OK]}

    FORMAT_JSON = "application/json"
    FORMAT_XML = "application/xml"

    def __init__(self, configuration):
        try:
            ssl = strtobool(configuration.UseSSL)
        except ValueNotFound:
            ssl = False

        self._links_factory = LinksFactory(configuration.ApiEndpoint, ssl=ssl)
        self._config_property_reader = configuration
        try:
            self.script_execution_id = ScriptDAO.get_last_script_execution_id()
            if self.script_execution_id != None:
                script_execution = ScriptDAO.get_script_execution_details(self.script_execution_id)
                if getppid() != script_execution.pid:
                    # this is probably replay or running test script without framework
                    # do not register requests
                    self.script_execution_id = None
        except:
            self.script_execution_id = None

        self.request_registration = self.script_execution_id is not None

    @property
    def links_factory(self):
        return self._links_factory

    @property
    def _is_controlled_by_framework(self):
        """
        Used to determine if test that executes api caller is controlled by CTS framework
        or has been executed as independent python script. This is used to determine if request/response should
        be written to database.

        :rtype: bool
        """
        return self.script_execution_id is not None

    def _perform_call(self, url, http_method=None, acceptable_return_codes=None,
                      payload=None, api_endpoint_override=None, format=None):
        """
        Helper method that executes http request and returns result. Internally it may talk to remote API or
        retrieve recorded data from database.
        :rtype: (Link, RequestStatus, int, json|String, dict)
        :type http_method: str
        :type acceptable_return_codes: list
        :type payload: object
        """
        if url is None:
            return None, RequestStatus.FAILED, None, None, None

        if format is None:
            format = ApiCaller.FORMAT_JSON

        if not http_method:
            http_method = HttpMethods.GET
        if not acceptable_return_codes:
            acceptable_return_codes = ApiCaller.DEFAULT_RETURN_CODES[http_method]

        try:
            link, kwargs = self._build_request(url, payload,
                                               api_endpoint_override=api_endpoint_override,
                                               format=format)
            url = link.link

            print "MESSAGE::-%s %s" % (http_method, url)
            if not self._is_controlled_by_framework:
                self._log_request(kwargs)

            response, status_code = self._do_request(kwargs, url, http_method)

            if response is None:
                return None, RequestStatus.FAILED, status_code, None, None

            self._register_request(http_method, kwargs, response, url)

            status = RequestStatus.SUCCESS if response.status_code in acceptable_return_codes else RequestStatus.FAILED
            status_code, headers = response.status_code, response.headers

            if status_code in range(500, 600):
                custom_response_text = HTTPServerErrors.ERROR.get(status_code, "Unknown server error: %s" % status_code)
                return None, RequestStatus.FAILED, status_code, custom_response_text, None

            if format == ApiCaller.FORMAT_JSON:
                response_body = self._decode_json(url, response)
                if response_body is None:
                    return None, RequestStatus.FAILED, None, None, None
            else:
                response_body = response.text

        except Exception as err:
            cts_error(
                "Unknown exception '{err:exception}' on {http_method} resource {url:id} : {"
                "stack:stacktrace}",
                stack=format_exc(), **locals())
            return None, RequestStatus.FAILED, None, None, None

        return link, status, status_code, response_body, headers

    def _decode_json(self, url, response):
        if response.text:
            if response.status_code in [ReturnCodes.NO_CONTENT]:
                cts_error(
                    "{method} {url:id} Non-empty response despite NO_CONTENT {code} return "
                    "code",
                    method=response.request.method, url=url,
                    code=ReturnCodes.NO_CONTENT)
            try:
                if isinstance(response.text, OrderedDict):
                    response_without_any_ordered_dict_collection = json.dumps(response.text)
                else:
                    response_without_any_ordered_dict_collection = response.text
                response_body = json.loads(response_without_any_ordered_dict_collection,
                                           object_pairs_hook=OrderedDict)
            except (JSONDecodeError, ValueError) as err:
                method = response.request.method if response.request else ''
                cts_error("{method} {url:id} Unable to parse. Error {err:exception}", method=method, url=url, err=err)
                return None
        else:
            if response.status_code in [ReturnCodes.NO_CONTENT, ReturnCodes.CREATED,
                                        ReturnCodes.ACCEPTED]:
                response_body = dict()
            else:
                cts_error("{url:id} Unexpected empty response", url=url)
                response_body = None

        if not self._is_controlled_by_framework:
            self._log_response(response, response_body)

        return response_body

    def _register_request(self, http_method, kwargs, response, url):
        # register request/response in the database
        if self._is_controlled_by_framework:
            # Remove stuff to make the serializers work:
            lkwargs = kwargs.copy()
            del lkwargs['hooks']
            response.request = None

            request_id = HttpRequestDAO.register_request(
                self.script_execution_id, http_method, url,
                json.dumps(lkwargs), pickle.dumps(response),
                response.status_code)
            print "%s::request_id=%d" % (LoggingLevel.CONTROL, request_id)

    def _do_request(self, kwargs, url, http_method):
        response = None
        if ReplayController.replay_mode_on():
            response = ReplayController.request(http_method, url, **kwargs)

        if response is None:
            requests_method = {HttpMethods.GET: requests.get,
                               HttpMethods.PATCH: requests.patch,
                               HttpMethods.POST: requests.post,
                               HttpMethods.DELETE: requests.delete}[http_method]

            try:
                self._suppress_urllib3_error()
                response = requests_method(url, **kwargs)
            except OpenSSL.SSL.Error as ose:
                cts_error("There is a problem with CertFile or KeyFile: {err}",
                          err=' '.join(ose.message[0]))
                return None, ReturnCodes.INVALID_CERTS
            except requests.HTTPError as err:
                cts_error("{method} {url:id} Error {err:exception}",
                          method=http_method, url=url, err=err)
                return None, ReturnCodes.METHOD_NOT_ALLOWED
            except requests.TooManyRedirects as err:
                cts_error("{method} {url:id} Error {err:exception}",
                          method=http_method, url=url, err=err)
                return None, ReturnCodes.INVALID_FORWARDING
            except requests.Timeout as err:
                cts_error("{method} {url:id} Error {err:exception}",
                          method=http_method, url=url, err=err)
                return None, ReturnCodes.TIMEOUT
            except requests.ConnectionError as err:
                cts_error("{method} {url:id} Error {err:exception}",
                          method=http_method, url=url, err=err)
                return None, ReturnCodes.NOT_FOUND
            except requests.RequestException as err:
                cts_error("{method} {url:id} Error {err:exception}",
                          method=http_method, url=url, err=err)
                return None, ReturnCodes.BAD_REQUEST
        return response, response.status_code

    @staticmethod
    def _suppress_urllib3_error():
        try:
            # Try to suppress user warnings
            from requests.packages.urllib3.exceptions import InsecureRequestWarning
            from requests.packages.urllib3 import disable_warnings
            disable_warnings(category=InsecureRequestWarning)
        except:
            pass

    @staticmethod
    def _log_request(kwargs):
        cts_message(" request parameters:")
        for key, value in kwargs.iteritems():
            cts_message("     {key}:{value}", key=str(key), value=str(value))

    @staticmethod
    def _log_response(response, response_body):
        pretty_response = json.dumps(response_body, indent=4) \
            if response.status_code != ReturnCodes.NO_CONTENT else "No content"
        cts_message("status code: %d" % response.status_code)
        cts_message("response:")
        for r in pretty_response.split('\n'):
            cts_message("{line}\n", line=r)

    @staticmethod
    def _skip_refresh_after_request(url):
        # we can not do GET on Actions/* without additional errors in logs
        return True if re.search(r"(redfish\/v1\/Nodes\/|Systems\/+)(.*Actions\/)", url) else False

    def get_xml(self, uri):
        return self._perform_call(uri, format=ApiCaller.FORMAT_XML)

    def get_resource(self, url, discovery_container, acceptable_return_codes=None,
                     api_endpoint_override=None, check_resource_against_metadata=None):
        """
        Sends http GET request to remote endpoint and retrieves resource odata_id.

        :type url: str
        :type discovery_container: DiscoveryContainer
        :type acceptable_return_codes: list(int)

        :rtype link
        :rtype status
        :rtype status_code
        :rtype response_body
        :rtype headers
        """

        link, status, status_code, response_body, headers = \
            self._perform_call(url,
                               acceptable_return_codes=acceptable_return_codes,
                               api_endpoint_override=api_endpoint_override)
        if not discovery_container:
            return link, status, status_code, response_body, headers

        if status == RequestStatus.SUCCESS:
            if response_body and status_code != ReturnCodes.NOT_FOUND:
                status = discovery_container.add_resource(
                    ApiResource(link.link,
                                link.netloc,
                                response_body,
                                discovery_container.get_expected_odata_type_for_url(link.link)),
                                    check_resource_against_metadata=check_resource_against_metadata)
        else:
            cts_error("{url:id} Get failed. Status code: {code}", url=url, code=status_code)

        return link, status, status_code, response_body, headers

    def post_resource(self, url, discovery_container, payload=None, acceptable_return_codes=None,
                      wait_if_async=True, expect_location=None, api_endpoint_override=None):
        """
        Sends http POST request to remote endpoint.
        Throws AsyncOperation if service created task in response to this request.

        :type url: str
        :type discovery_container: DiscoveryContainer
        :type payload: dict
        :type acceptable_return_codes: list(int)
        :type wait_if_async: bool
        """
        from cts_core.discovery.api_explorer import ApiExplorer

        if expect_location is None:
            expect_location = True

        _, status, status_code, response_body, headers = \
            self._perform_call(url,
                               http_method=HttpMethods.POST,
                               payload=payload,
                               acceptable_return_codes=acceptable_return_codes,
                               api_endpoint_override=api_endpoint_override)
        # determine if the operation was completed synchronously or asynchronously
        if status_code == ReturnCodes.ACCEPTED:
            if not wait_if_async:
                raise AsyncOperation()
            status, status_code, response_body, headers = self._wait_for_task(headers,
                                                                              discovery_container,
                                                                              acceptable_ret_codes=acceptable_return_codes)

        if expect_location and status and not self._skip_refresh_after_request(url):
            try:
                # add the created element and discover all its children
                new_url = self.links_factory.get_resource_link(headers["Location"],
                                                               api_endpoint_override=api_endpoint_override).link
                print "MESSAGE::Refreshing {} info".format(new_url)
                # refresh the collection info
                _, get_status, get_status_code, _, _ = self.get_resource(url, discovery_container,
                                                                         api_endpoint_override=api_endpoint_override,
                                                                         check_resource_against_metadata=True)
                _, rediscovery_status = ApiExplorer(discovery_container.metadata_container,
                                                    self._config_property_reader).discover(new_url,
                                                                                           discovery_container.get_expected_odata_type_for_url(new_url),
                                                                                           discovery_container)
                if get_status and rediscovery_status:
                    print "MESSAGE::Refreshed %s and its children info" % new_url
                else:
                    cts_warning("Refreshing {odata_id:id} after POST generated errors. Get status code: {code}",
                                odata_id=new_url, code=get_status_code)

            except (KeyError, TypeError) as err:
                cts_warning(
                    "POST {odata_id:id} Response has no 'Location' header; Error: {err:exception}",
                    odata_id=url, err=err)

        return status, status_code, response_body, headers

    def patch_resource(self, url, discovery_container, payload=None,
                       acceptable_return_codes=None, wait_if_async=True, api_endpoint_override=None):
        """
        Sends http PATCH request to remote endpoint.
        Throws AsyncOperation if service created task in response to this request.

        :type url: str
        :type discovery_container: DiscoveryContainer
        :type payload: dict
        :type acceptable_return_codes: list(int)
        :type wait_if_async: bool
        """

        _, patch_status, patch_status_code, response_body, headers = \
            self._perform_call(url,
                               http_method=HttpMethods.PATCH,
                               payload=payload,
                               acceptable_return_codes=acceptable_return_codes,
                               api_endpoint_override=api_endpoint_override)

        # determine if the operation was completed synchronously or asynchronously
        if patch_status_code == ReturnCodes.ACCEPTED:
            if not wait_if_async:
                raise AsyncOperation()
            patch_status, patch_status_code, response_body, headers = self._wait_for_task(headers,
                                                                                          discovery_container,
                                                                                          acceptable_ret_codes=acceptable_return_codes)

        if patch_status != RequestStatus.SUCCESS:
            cts_error("{url:id} Patch failed. Status code: {code}", url=url, code=patch_status_code)

        print "MESSAGE::Refreshing {} info".format(url)

        _, get_status, get_status_code, _, _ = self.get_resource(url, discovery_container,
                                                                 api_endpoint_override=api_endpoint_override,
                                                                 check_resource_against_metadata=True)
        if not get_status:
            cts_warning("Refreshing {url:id} after PATCH generated errors. Get status code: {code}",
                        url=url, code=get_status_code)

        return patch_status, patch_status_code, response_body, headers

    def delete_resource(self, url, discovery_container, acceptable_return_codes=None,
                        wait_if_async=True, api_endpoint_override=None):
        """
        Sends http DELETE request to remote endpoint.
        Throws AsyncOperation if service created task in response to this request.

        :type url: str
        :type discovery_container: DiscoveryContainer
        :type acceptable_return_codes: list(int)
        :type wait_if_async: bool
        """
        link, status, status_code, response_body, headers = \
            self._perform_call(url,
                               http_method=HttpMethods.DELETE,
                               acceptable_return_codes=acceptable_return_codes,
                               api_endpoint_override=api_endpoint_override)

        # determine if the operation was completed synchronously or asynchronously
        if status_code == ReturnCodes.ACCEPTED:
            if not wait_if_async:
                raise AsyncOperation()
            status, status_code, response_body, headers = self._wait_for_task(headers,
                                                                              discovery_container,
                                                                              acceptable_ret_codes=acceptable_return_codes)

        # In UNPICKLE mode, CTS can't find a parent resource, so return mocked success
        if getenv('CTS_UNPICKLE', None):
            return MockConstants.delete_resource()

        if status == RequestStatus.SUCCESS:
            if discovery_container[link.link].parent_url:
                self.get_resource(discovery_container[link.link].parent_url,
                                  discovery_container,
                                  api_endpoint_override=api_endpoint_override)
            else:
                cts_warning("Unable to update the parent of {url:id} - parent unknown",
                            url=link.link)
            discovery_container.delete_resource(link.link)
        else:
            # not deleted. refresh
            self.get_resource(url, discovery_container,
                              api_endpoint_override=api_endpoint_override)

        return status, status_code, response_body, headers

    def _wait_for_task(self, headers, discovery_container, api_endpoint_override=None, acceptable_ret_codes=None):
        if acceptable_ret_codes is None:
            acceptable_ret_codes = [ReturnCodes.OK,
                                    ReturnCodes.CREATED,
                                    ReturnCodes.NO_CONTENT
                                    ]
        task_monitor = headers.get('Location')
        print "MESSAGE::Waiting for task completion. Monitor = %s" % task_monitor
        if task_monitor:
            for _ in range(0, TASK_TIMEOUT):
                _, status, status_code, task, headers = \
                    self.get_resource(task_monitor,
                                      discovery_container,
                                      acceptable_ret_codes +
                                      [
                                          ReturnCodes.ACCEPTED
                                      ],
                                      api_endpoint_override)

                # The client may also cancel the operation by performing a DELETE on the Task resource. Deleting the
                # Task resource object may invalidate the associated Task Monitor and subsequent GET on the Task
                # Monitor URL returns either 410 (Gone) or 404 (Not Found)
                if status_code in [ReturnCodes.NOT_FOUND, ReturnCodes.GONE]:
                    cts_warning("Task was cancelled unexpectedly")
                    return RequestStatus.FAILED, None, None, None

                # Once the operation has completed, the Task Monitor shall return a status code of OK (200) or
                # CREATED (201) for POST and include the headers and response body of the initial operation, as if it
                #  had completed synchronously
                if status_code in acceptable_ret_codes:
                    return status, status_code, task, headers

                # As long as the operation is in process, the service shall continue to return a status code of
                # 202 (Accepted) when querying the Task Monitor returned in the location header
                if status_code not in [ReturnCodes.ACCEPTED]:
                    cts_error("{odata_id:id} Task monitor returned unexpected status code: {code}",
                              odata_id=task_monitor, code=status_code)
                    return RequestStatus.FAILED, None, None, None

                print "MESSAGE::Task in progress. Waiting for completion"
                time.sleep(1)
        else:
            cts_error("{odata_id:id} Task has been created but Location header not found",
                      odata_id=task_monitor)
            return RequestStatus.FAILED, None, None, None

    def _build_request(self, url, payload=None, api_endpoint_override=None, format=None):
        if format is None:
            format = ApiCaller.FORMAT_JSON

        link = self._links_factory.get_resource_link(url,
                                                     api_endpoint_override=api_endpoint_override)

        kwargs = {'verify': False,
                  'headers': {"Content-Type": format,
                              "Accept": format},
                  'hooks': {'response': self._save_address_hook},
                  }
        try:
            kwargs["cert"] = (self._config_property_reader.CertificateCertFile,
                              self._config_property_reader.CertificateKeyFile)
        except ValueNotFound:
            # not using certificate authorization
            pass
        try:
            kwargs["auth"] = (
                self._config_property_reader.User, self._config_property_reader.Password)
        except ValueNotFound:
            pass

        if payload:
            kwargs["data"] = json.dumps(payload)

        return link, kwargs

    @staticmethod
    def read_certificate(certificate_file):
        try:
            with open(certificate_file, "r") as f:
                return f.read()
        except IOError:
            pass

        return None

    def _save_address_hook(self, r, *args, **kwargs):
        s = socket.fromfd(r.raw.fileno(), socket.AF_INET, socket.SOCK_STREAM)
        self.remote_address = s.getpeername()
        self.local_address = s.getsockname()
예제 #8
0
class ApiCaller:
    DEFAULT_RETURN_CODES = {
        HttpMethods.PATCH: [ReturnCodes.NO_CONTENT, ReturnCodes.ACCEPTED, ReturnCodes.OK],
        HttpMethods.POST: [ReturnCodes.CREATED, ReturnCodes.ACCEPTED],
        HttpMethods.DELETE: [ReturnCodes.NO_CONTENT, ReturnCodes.ACCEPTED],
        HttpMethods.GET: [ReturnCodes.OK]}

    FORMAT_JSON = "application/json"
    FORMAT_XML = "application/xml"

    def __init__(self, configuration):
        try:
            ssl = strtobool(configuration.UseSSL)
        except ValueNotFound:
            ssl = False

        self._links_factory = LinksFactory(configuration.ApiEndpoint, ssl=ssl)
        self._config_property_reader = configuration
        try:
            self.script_execution_id = ScriptDAO.get_last_script_execution_id()
            if self.script_execution_id != None:
                script_execution = ScriptDAO.get_script_execution_details(self.script_execution_id)
                if getppid() != script_execution.pid:
                    # this is probably replay or running test script without framework
                    # do not register requests
                    self.script_execution_id = None
        except:
            self.script_execution_id = None

        self.request_registration = self.script_execution_id is not None

    @property
    def links_factory(self):
        return self._links_factory

    @property
    def _is_controlled_by_framework(self):
        """
        Used to determine if test that executes api caller is controlled by CTS framework
        or has been executed as independent python script. This is used to determine if request/response should
        be written to database.

        :rtype: bool
        """
        return self.script_execution_id is not None

    def _perform_call(self, url, http_method=None, acceptable_return_codes=None,
                      payload=None, api_endpoint_override=None, format=None):
        """
        Helper method that executes http request and returns result. Internally it may talk to remote API or
        retrieve recorded data from database.
        :rtype: (Link, RequestStatus, int, json|String, dict)
        :type http_method: str
        :type acceptable_return_codes: list
        :type payload: object
        """
        if url is None:
            return None, RequestStatus.FAILED, None, None, None

        if format is None:
            format = ApiCaller.FORMAT_JSON

        if not http_method:
            http_method = HttpMethods.GET
        if not acceptable_return_codes:
            acceptable_return_codes = ApiCaller.DEFAULT_RETURN_CODES[http_method]

        try:
            link, kwargs = self._build_request(url, payload,
                                               api_endpoint_override=api_endpoint_override,
                                               format=format)
            url = link.link

            print "MESSAGE::-%s %s" % (http_method, url)
            if not self._is_controlled_by_framework:
                self._log_request(kwargs)

            response = self._do_request(kwargs, url, http_method)

            if response is None:
                return None, RequestStatus.FAILED, None, None, None

            self._register_request(http_method, kwargs, response, url)

            status = RequestStatus.SUCCESS if response.status_code in acceptable_return_codes else RequestStatus.FAILED
            status_code, headers = response.status_code, response.headers

            if status_code in range(500, 600):
                custom_response_text = HTTPServerErrors.ERROR.get(status_code, "Unknown server error: %s" % status_code)
                return None, RequestStatus.FAILED, status_code, custom_response_text, None

            if format == ApiCaller.FORMAT_JSON:
                response_body = self._decode_json(url, response)
                if response_body is None:
                    return None, RequestStatus.FAILED, None, None, None
            else:
                response_body = response.text

        except Exception as err:
            cts_error(
                "Unknown exception '{err:exception}' on {http_method} resource {url:id} : {"
                "stack:stacktrace}",
                stack=format_exc(), **locals())
            return None, RequestStatus.FAILED, None, None, None

        return link, status, status_code, response_body, headers

    def _decode_json(self, url, response):
        if response.text:
            if response.status_code in [ReturnCodes.NO_CONTENT]:
                cts_error(
                    "{method} {url:id} Non-empty response despite NO_CONTENT {code} return "
                    "code",
                    method=response.request.method, url=url,
                    code=ReturnCodes.NO_CONTENT)
            try:
                response_body = json.loads(response.text, object_pairs_hook=OrderedDict)
            except (JSONDecodeError, ValueError) as err:
                method = response.request.method if response.request else ''
                cts_error("{method} {url:id} Unable to parse. Error {err:exception}", method=method, url=url, err=err)
                return None
        else:
            if response.status_code in [ReturnCodes.NO_CONTENT, ReturnCodes.CREATED,
                                        ReturnCodes.ACCEPTED]:
                response_body = dict()
            else:
                cts_error("{url:id} Unexpected empty response", url=url)
                response_body = None

        if not self._is_controlled_by_framework:
            self._log_response(response, response_body)

        return response_body

    def _register_request(self, http_method, kwargs, response, url):
        # register request/response in the database
        if self._is_controlled_by_framework:
            # Remove stuff to make the serializers work:
            lkwargs = kwargs.copy()
            del lkwargs['hooks']
            response.request = None

            request_id = HttpRequestDAO.register_request(
                self.script_execution_id, http_method, url,
                json.dumps(lkwargs), pickle.dumps(response),
                response.status_code)
            print "%s::request_id=%d" % (LoggingLevel.CONTROL, request_id)

    def _do_request(self, kwargs, url, http_method):
        if ReplayController.replay_mode_on():
            response = ReplayController.request(http_method, url, **kwargs)
        else:
            response = None

        if response is None:
            requests_method = {HttpMethods.GET: requests.get, HttpMethods.PATCH: requests.patch,
                               HttpMethods.POST: requests.post,
                               HttpMethods.DELETE: requests.delete}[http_method]

            try:
                self._suppress_urllib3_error()

                response = requests_method(url, **kwargs)
            except OpenSSL.SSL.Error as ose:
                cts_error("There is a problem with CertFile or KeyFile: {err}", err=' '.join(ose.message[0]))
            except requests.RequestException as err:
                cts_error("{method} {url:id} Error {err:exception}",
                          method=http_method, url=url, err=err)
                return None

        return response

    def _suppress_urllib3_error(self):
        try:
            # Try to suppress user warnings
            from requests.packages.urllib3.exceptions import InsecureRequestWarning
            from requests.packages.urllib3 import disable_warnings
            disable_warnings(InsecureRequestWarning)
        except:
            pass

    def _log_request(self, kwargs):
        cts_message(" request parameters:")
        for key, value in kwargs.iteritems():
            cts_message("     {key}:{value}", key=str(key), value=str(value))

    def _log_response(self, response, response_body):
        pretty_response = json.dumps(response_body, indent=4) \
            if response.status_code != ReturnCodes.NO_CONTENT else "No content"
        cts_message("status code: %d" % response.status_code)
        cts_message("response:")
        for r in pretty_response.split('\n'):
            cts_message("{line}\n", line=r)

    @staticmethod
    def _skip_refresh_after_request(url):
        # we can not do GET on Actions/* without additional errors in logs
        return True if re.search(r"(redfish\/v1\/Nodes\/|Systems\/+)(.*Actions\/)", url) else False

    def get_xml(self, uri):
        return self._perform_call(uri, format=ApiCaller.FORMAT_XML)

    def get_resource(self, url, discovery_container, acceptable_return_codes=None,
                     api_endpoint_override=None, check_resource_against_metadata=None):
        """
        Sends http GET request to remote endpoint and retrieves resource odata_id.

        :type url: str
        :type discovery_container: DiscoveryContainer
        :type acceptable_return_codes: list(int)

        :rtype link
        :rtype status
        :rtype status_code
        :rtype response_body
        :rtype headers
        """

        link, status, status_code, response_body, headers = \
            self._perform_call(url,
                               acceptable_return_codes=acceptable_return_codes,
                               api_endpoint_override=api_endpoint_override)
        if status == RequestStatus.SUCCESS:
            if response_body and status_code != ReturnCodes.NOT_FOUND:
                status = discovery_container.add_resource(
                    ApiResource(link.link,
                                link.netloc,
                                response_body,
                                discovery_container.get_expected_odata_type_for_url(link.link)),
                                    check_resource_against_metadata=check_resource_against_metadata)
        else:
            cts_error("{url:id} Get failed. Status code: {code}", url=url, code=status_code)

        return link, status, status_code, response_body, headers

    def post_resource(self, url, discovery_container, payload=None, acceptable_return_codes=None,
                      wait_if_async=True, expect_location=None, api_endpoint_override=None):
        """
        Sends http POST request to remote endpoint.
        Throws AsyncOperation if service created task in response to this request.

        :type url: str
        :type discovery_container: DiscoveryContainer
        :type payload: dict
        :type acceptable_return_codes: list(int)
        :type wait_if_async: bool
        """
        from cts_core.discovery.api_explorer import ApiExplorer

        if expect_location is None:
            expect_location = True

        _, status, status_code, response_body, headers = \
            self._perform_call(url,
                               http_method=HttpMethods.POST,
                               payload=payload,
                               acceptable_return_codes=acceptable_return_codes,
                               api_endpoint_override=api_endpoint_override)
        # determine if the operation was completed synchronously or asynchronously
        if status_code == ReturnCodes.ACCEPTED:
            if not wait_if_async:
                raise AsyncOperation()
            status, status_code, response_body, headers = self._wait_for_task(headers,
                                                                              discovery_container,
                                                                              acceptable_ret_codes=acceptable_return_codes)

        if expect_location and status and not self._skip_refresh_after_request(url):
            try:
                # add the created element and discover all its children
                new_url = self.links_factory.get_resource_link(headers["Location"],
                                                               api_endpoint_override=api_endpoint_override).link
                print "MESSAGE::Refreshing {} info".format(new_url)
                # refresh the collection info
                _, get_status, get_status_code, _, _ = self.get_resource(url, discovery_container,
                                                                         api_endpoint_override=api_endpoint_override,
                                                                         check_resource_against_metadata=True)
                _, rediscovery_status = ApiExplorer(discovery_container.metadata_container,
                                                    self._config_property_reader).discover(new_url,
                                                                                           discovery_container.get_expected_odata_type_for_url(new_url),
                                                                                           discovery_container)
                if get_status and rediscovery_status:
                    print "MESSAGE::Refreshed %s and its children info" % new_url
                else:
                    cts_warning("Refreshing {odata_id:id} after POST generated errors. Get status code: {code}",
                                odata_id=new_url, code=get_status_code)

            except (KeyError, TypeError) as err:
                cts_warning(
                    "POST {odata_id:id} Response has no 'Location' header; Error: {err:exception}",
                    odata_id=url, err=err)

        return status, status_code, response_body, headers

    def patch_resource(self, url, discovery_container, payload=None,
                       acceptable_return_codes=None, wait_if_async=True, api_endpoint_override=None):
        """
        Sends http PATCH request to remote endpoint.
        Throws AsyncOperation if service created task in response to this request.

        :type url: str
        :type discovery_container: DiscoveryContainer
        :type payload: dict
        :type acceptable_return_codes: list(int)
        :type wait_if_async: bool
        """

        _, patch_status, patch_status_code, response_body, headers = \
            self._perform_call(url,
                               http_method=HttpMethods.PATCH,
                               payload=payload,
                               acceptable_return_codes=acceptable_return_codes,
                               api_endpoint_override=api_endpoint_override)

        # determine if the operation was completed synchronously or asynchronously
        if patch_status_code == ReturnCodes.ACCEPTED:
            if not wait_if_async:
                raise AsyncOperation()
            patch_status, patch_status_code, response_body, headers = self._wait_for_task(headers,
                                                                                          discovery_container,
                                                                                          acceptable_ret_codes=acceptable_return_codes)

        if patch_status != RequestStatus.SUCCESS:
            cts_error("{url:id} Patch failed. Status code: {code}", url=url, code=patch_status_code)

        print "MESSAGE::Refreshing {} info".format(url)

        _, get_status, get_status_code, _, _ = self.get_resource(url, discovery_container,
                                                                 api_endpoint_override=api_endpoint_override,
                                                                 check_resource_against_metadata=True)
        if not get_status:
            cts_warning("Refreshing {url:id} after PATCH generated errors. Get status code: {code}",
                        url=url, code=get_status_code)

        return patch_status, patch_status_code, response_body, headers

    def delete_resource(self, url, discovery_container, acceptable_return_codes=None,
                        wait_if_async=True, api_endpoint_override=None):
        """
        Sends http DELETE request to remote endpoint.
        Throws AsyncOperation if service created task in response to this request.

        :type url: str
        :type discovery_container: DiscoveryContainer
        :type acceptable_return_codes: list(int)
        :type wait_if_async: bool
        """
        link, status, status_code, response_body, headers = \
            self._perform_call(url,
                               http_method=HttpMethods.DELETE,
                               acceptable_return_codes=acceptable_return_codes,
                               api_endpoint_override=api_endpoint_override)

        # determine if the operation was completed synchronously or asynchronously
        if status_code == ReturnCodes.ACCEPTED:
            if not wait_if_async:
                raise AsyncOperation()
            status, status_code, response_body, headers = self._wait_for_task(headers,
                                                                              discovery_container,
                                                                              acceptable_ret_codes=acceptable_return_codes)

        # In UNPICKLE mode, CTS can't find a parent resource, so return mocked success
        if getenv('CTS_UNPICKLE', None):
            return MockConstants.delete_resource()

        if status == RequestStatus.SUCCESS:
            if discovery_container[link.link].parent_url:
                self.get_resource(discovery_container[link.link].parent_url,
                                  discovery_container,
                                  api_endpoint_override=api_endpoint_override)
            else:
                cts_warning("Unable to update the parent of {url:id} - parent unknown",
                            url=link.link)
            discovery_container.delete_resource(link.link)
        else:
            # not deleted. refresh
            self.get_resource(url, discovery_container,
                              api_endpoint_override=api_endpoint_override)

        return status, status_code, response_body, headers

    def _wait_for_task(self, headers, discovery_container, api_endpoint_override=None, acceptable_ret_codes=None):
        if acceptable_ret_codes is None:
            acceptable_ret_codes = [ReturnCodes.OK,
                                    ReturnCodes.CREATED,
                                    ReturnCodes.NO_CONTENT
                                    ]
        task_monitor = headers.get('Location')
        print "MESSAGE::Waiting for task completion. Monitor = %s" % task_monitor
        if task_monitor:
            for _ in range(0, TASK_TIMEOUT):
                _, status, status_code, task, headers = \
                    self.get_resource(task_monitor,
                                      discovery_container,
                                      acceptable_ret_codes +
                                      [
                                          ReturnCodes.ACCEPTED
                                      ],
                                      api_endpoint_override)

                # The client may also cancel the operation by performing a DELETE on the Task resource. Deleting the
                # Task resource object may invalidate the associated Task Monitor and subsequent GET on the Task
                # Monitor URL returns either 410 (Gone) or 404 (Not Found)
                if status_code in [ReturnCodes.NOT_FOUND, ReturnCodes.GONE]:
                    cts_warning("Task was cancelled unexpectedly")
                    return RequestStatus.FAILED, None, None, None

                # Once the operation has completed, the Task Monitor shall return a status code of OK (200) or
                # CREATED (201) for POST and include the headers and response body of the initial operation, as if it
                #  had completed synchronously
                if status_code in acceptable_ret_codes:
                    return status, status_code, task, headers

                # As long as the operation is in process, the service shall continue to return a status code of
                # 202 (Accepted) when querying the Task Monitor returned in the location header
                if status_code not in [ReturnCodes.ACCEPTED]:
                    cts_error("{odata_id:id} Task monitor returned unexpected status code: {code}",
                              odata_id=task_monitor, code=status_code)
                    return RequestStatus.FAILED, None, None, None

                print "MESSAGE::Task in progress. Waiting for completion"
                time.sleep(1)
        else:
            cts_error("{odata_id:id} Task has been created but Location header not found",
                      odata_id=task_monitor)
            return RequestStatus.FAILED, None, None, None

    def _build_request(self, url, payload=None, api_endpoint_override=None, format=None):
        if format is None:
            format = ApiCaller.FORMAT_JSON

        link = self._links_factory.get_resource_link(url,
                                                     api_endpoint_override=api_endpoint_override)

        kwargs = {'verify': False,
                  'headers': {"Content-Type": format,
                              "Accept": format},
                  'hooks': {'response': self._save_address_hook},
                  }
        try:
            kwargs["cert"] = (self._config_property_reader.CertificateCertFile,
                              self._config_property_reader.CertificateKeyFile)
        except ValueNotFound:
            # not using certificate authorization
            pass
        try:
            kwargs["auth"] = (
                self._config_property_reader.User, self._config_property_reader.Password)
        except ValueNotFound:
            pass

        if payload:
            kwargs["data"] = json.dumps(payload)

        return link, kwargs

    def read_certificate(self, certificate_file):
        try:
            with open(certificate_file, "r") as f:
                return f.read()
        except IOError:
            pass

        return None

    def _save_address_hook(self, r, *args, **kwargs):
        s = socket.fromfd(r.raw.fileno(), socket.AF_INET, socket.SOCK_STREAM)
        self.remote_address = s.getpeername()
        self.local_address = s.getsockname()
예제 #9
0
 def __init__(self, config_property_reader):
     self._links_factory = LinksFactory(config_property_reader.ApiEndpoint)
     self._config_property_reader = config_property_reader
예제 #10
0
class ApiCaller:
    def __init__(self, config_property_reader):
        self._links_factory = LinksFactory(config_property_reader.ApiEndpoint)
        self._config_property_reader = config_property_reader

    def perform_call(self, odata_id, http_method=None, acceptable_return_codes=None, payload=None):
        if not acceptable_return_codes:
            acceptable_return_codes = [ReturnCodes.OK]

        if not http_method:
            http_method = HttpMethods.GET

        resource = self._links_factory.get_resource_link(odata_id)

        try:
            params, kwargs = self.build_request_params(resource, payload)
            requests_method = {HttpMethods.GET: requests.get, HttpMethods.PATCH: requests.patch}[http_method]

            print "DEBUG::about to send request %s using parameters %s" % (http_method, ", ".join(
                [str(param) for param in params] + ["%s: %s" % (str(key), str(value)) for key, value in kwargs.iteritems()]))

            try:
                # Try to suppress user warnings
                urllib3.disable_warnings()
            except:
                pass

            response = requests_method(*params, **kwargs)
            status = RequestStatus.SUCCESS if response.status_code in acceptable_return_codes else RequestStatus.FAILED
            status_code, headers = response.status_code, response.headers

            content = response.text if response.status_code != ReturnCodes.NO_CONTENT else "No content"
            print "DEBUG::request %s on resource %s returned content %s" % (http_method, resource, content)

            if response.status_code in [ReturnCodes.NO_CONTENT, ReturnCodes.CREATED]:
                response_body = dict()
            else:
                response_body = response.json()

        except requests.RequestException as err:
            raise TestCaseFatalErrorFlag("Error: %s occurred when accessing resource %s" % (err.message, odata_id))
        except JSONDecodeError:
            raise TestCaseFatalErrorFlag("Unable to parse resource %s to json" % odata_id)
        except Exception as err:
            raise TestCaseFatalErrorFlag("Unknown exception %s while accessing resource %s" % (err.message, odata_id))

        return status, status_code, response_body, headers

    def get_resource(self, odata_id):
        return self.perform_call(odata_id)

    def build_request_params(self, resource, payload):
        kwargs = dict(verify=False, headers={"Content-Type": "application/json"})
        try:
            kwargs["cert"] = (self._config_property_reader.CertificateCertFile,
                              self._config_property_reader.CertificateKeyFile)
        except ValueNotFound:
            #not using certificate authorization
            pass

        try:
            kwargs["auth"] = (self._config_property_reader.User, self._config_property_reader.Password)
        except ValueNotFound:
            pass

        try:
            if strtobool(self._config_property_reader.UseSSL):
                params = HTTPS + resource
            else:
                params = HTTP + resource
        except ValueNotFound:
            params = HTTP + resource

        if payload:
            kwargs["data"] = json.dumps(payload)

        return [params], kwargs

    def read_certificate(self, certificate_file):
        try:
            with open(certificate_file, "r") as f:
                return f.read()
        except Exception:
            return None
예제 #11
0
 def setUp(self):
     self.links_factory = LinksFactory(ENDPOINT, ssl=True)
예제 #12
0
 def setUp(self):
     self.links_factory = LinksFactory(ENDPOINT, ssl=True)