diff options
author | Ghanshyam Mann <gmann@ghanshyammann.com> | 2022-04-05 17:38:00 -0500 |
---|---|---|
committer | Ghanshyam <gmann@ghanshyammann.com> | 2022-04-05 23:41:14 +0000 |
commit | fc1791f8589cb8dba9d8d9ef7856be17b2bc77f3 (patch) | |
tree | 034f0b3ccd9559c3227abac0e54abbd3f4f67920 /tempest_lib/common/rest_client.py | |
parent | 023426894a4f72d906ed6f79c55ed7152a732b44 (diff) | |
download | tempest-lib-master.tar.gz |
As of the 1.0.0 release tempest-lib as a separate
repository and project is deprecated. We moved all the
code to tempest.lib temepst version 10.
Hoping everyone is moved to tempest.lib now, We can retire
the tempest-lib. We discussed the same in PTG and agreed to
proceed on retirement
- https://etherpad.opendev.org/p/qa-zed-ptg
Needed-By: https://review.opendev.org/c/openstack/governance/+/836704
Change-Id: I37ceb96e084f569ea59e4849ca8770742ce17846
Diffstat (limited to 'tempest_lib/common/rest_client.py')
-rw-r--r-- | tempest_lib/common/rest_client.py | 894 |
1 files changed, 0 insertions, 894 deletions
diff --git a/tempest_lib/common/rest_client.py b/tempest_lib/common/rest_client.py deleted file mode 100644 index c5886e6..0000000 --- a/tempest_lib/common/rest_client.py +++ /dev/null @@ -1,894 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# Copyright 2013 IBM Corp. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import collections -import logging as real_logging -import re -import time - -import jsonschema -from oslo_log import log as logging -from oslo_serialization import jsonutils as json -import six - -from tempest_lib.common import http -from tempest_lib.common.utils import misc as misc_utils -from tempest_lib import exceptions - -# redrive rate limited calls at most twice -MAX_RECURSION_DEPTH = 2 - -# All the successful HTTP status codes from RFC 7231 & 4918 -HTTP_SUCCESS = (200, 201, 202, 203, 204, 205, 206, 207) - -# All the redirection HTTP status codes from RFC 7231 & 4918 -HTTP_REDIRECTION = (300, 301, 302, 303, 304, 305, 306, 307) - -# JSON Schema validator and format checker used for JSON Schema validation -JSONSCHEMA_VALIDATOR = jsonschema.Draft4Validator -FORMAT_CHECKER = jsonschema.draft4_format_checker - - -class RestClient(object): - """Unified OpenStack RestClient class - - This class is used for building openstack api clients on top of. It is - intended to provide a base layer for wrapping outgoing http requests in - keystone auth as well as providing response code checking and error - handling. - - :param auth_provider: an auth provider object used to wrap requests in auth - :param str service: The service name to use for the catalog lookup - :param str region: The region to use for the catalog lookup - :param str endpoint_type: The endpoint type to use for the catalog lookup - :param int build_interval: Time in seconds between to status checks in - wait loops - :param int build_timeout: Timeout in seconds to wait for a wait operation. - :param bool disable_ssl_certificate_validation: Set to true to disable ssl - certificate validation - :param str ca_certs: File containing the CA Bundle to use in verifying a - TLS server cert - :param str trace_request: Regex to use for specifying logging the entirety - of the request and response payload - """ - TYPE = "json" - - # The version of the API this client implements - api_version = None - - LOG = logging.getLogger(__name__) - - def __init__(self, auth_provider, service, region, - endpoint_type='publicURL', - build_interval=1, build_timeout=60, - disable_ssl_certificate_validation=False, ca_certs=None, - trace_requests=''): - self.auth_provider = auth_provider - self.service = service - self.region = region - self.endpoint_type = endpoint_type - self.build_interval = build_interval - self.build_timeout = build_timeout - self.trace_requests = trace_requests - - self._skip_path = False - self.general_header_lc = set(('cache-control', 'connection', - 'date', 'pragma', 'trailer', - 'transfer-encoding', 'via', - 'warning')) - self.response_header_lc = set(('accept-ranges', 'age', 'etag', - 'location', 'proxy-authenticate', - 'retry-after', 'server', - 'vary', 'www-authenticate')) - dscv = disable_ssl_certificate_validation - self.http_obj = http.ClosingHttp( - disable_ssl_certificate_validation=dscv, ca_certs=ca_certs) - - def _get_type(self): - return self.TYPE - - def get_headers(self, accept_type=None, send_type=None): - """Return the default headers which will be used with outgoing requests - - :param str accept_type: The media type to use for the Accept header, if - one isn't provided the object var TYPE will be - used - :param str send_type: The media-type to use for the Content-Type - header, if one isn't provided the object var - TYPE will be used - :rtype: dict - :return: The dictionary of headers which can be used in the headers - dict for outgoing request - """ - if accept_type is None: - accept_type = self._get_type() - if send_type is None: - send_type = self._get_type() - return {'Content-Type': 'application/%s' % send_type, - 'Accept': 'application/%s' % accept_type} - - def __str__(self): - STRING_LIMIT = 80 - str_format = ("service:%s, base_url:%s, " - "filters: %s, build_interval:%s, build_timeout:%s" - "\ntoken:%s..., \nheaders:%s...") - return str_format % (self.service, self.base_url, - self.filters, self.build_interval, - self.build_timeout, - str(self.token)[0:STRING_LIMIT], - str(self.get_headers())[0:STRING_LIMIT]) - - @property - def user(self): - """The username used for requests - - :rtype: string - :return: The username being used for requests - """ - - return self.auth_provider.credentials.username - - @property - def user_id(self): - """The user_id used for requests - - :rtype: string - :return: The user id being used for requests - """ - return self.auth_provider.credentials.user_id - - @property - def tenant_name(self): - """The tenant/project being used for requests - - :rtype: string - :return: The tenant/project name being used for requests - """ - return self.auth_provider.credentials.tenant_name - - @property - def tenant_id(self): - """The tenant/project id being used for requests - - :rtype: string - :return: The tenant/project id being used for requests - """ - return self.auth_provider.credentials.tenant_id - - @property - def password(self): - """The password being used for requests - - :rtype: string - :return: The password being used for requests - """ - return self.auth_provider.credentials.password - - @property - def base_url(self): - return self.auth_provider.base_url(filters=self.filters) - - @property - def token(self): - return self.auth_provider.get_token() - - @property - def filters(self): - _filters = dict( - service=self.service, - endpoint_type=self.endpoint_type, - region=self.region - ) - if self.api_version is not None: - _filters['api_version'] = self.api_version - if self._skip_path: - _filters['skip_path'] = self._skip_path - return _filters - - def skip_path(self): - """When set, ignore the path part of the base URL from the catalog""" - self._skip_path = True - - def reset_path(self): - """When reset, use the base URL from the catalog as-is""" - self._skip_path = False - - @classmethod - def expected_success(cls, expected_code, read_code): - """Check expected success response code against the http response - - :param int expected_code: The response code that is expected. - Optionally a list of integers can be used - to specify multiple valid success codes - :param int read_code: The response code which was returned in the - response - :raises AssertionError: if the expected_code isn't a valid http success - response code - :raises exceptions.InvalidHttpSuccessCode: if the read code isn't an - expected http success code - """ - assert_msg = ("This function only allowed to use for HTTP status" - "codes which explicitly defined in the RFC 7231 & 4918." - "{0} is not a defined Success Code!" - ).format(expected_code) - if isinstance(expected_code, list): - for code in expected_code: - assert code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg - else: - assert expected_code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg - - # NOTE(afazekas): the http status code above 400 is processed by - # the _error_checker method - if read_code < 400: - pattern = """Unexpected http success status code {0}, - The expected status code is {1}""" - if ((not isinstance(expected_code, list) and - (read_code != expected_code)) or - (isinstance(expected_code, list) and - (read_code not in expected_code))): - details = pattern.format(read_code, expected_code) - raise exceptions.InvalidHttpSuccessCode(details) - - def post(self, url, body, headers=None, extra_headers=False): - """Send a HTTP POST request using keystone auth - - :param str url: the relative url to send the post request to - :param dict body: the request body - :param dict headers: The headers to use for the request - :param dict extra_headers: If the headers returned by the get_headers() - method are to be used but additional headers - are needed in the request pass them in as a - dict - :return: a tuple with the first entry containing the response headers - and the second the response body - :rtype: tuple - """ - return self.request('POST', url, extra_headers, headers, body) - - def get(self, url, headers=None, extra_headers=False): - """Send a HTTP GET request using keystone service catalog and auth - - :param str url: the relative url to send the post request to - :param dict headers: The headers to use for the request - :param dict extra_headers: If the headers returned by the get_headers() - method are to be used but additional headers - are needed in the request pass them in as a - dict - :return: a tuple with the first entry containing the response headers - and the second the response body - :rtype: tuple - """ - return self.request('GET', url, extra_headers, headers) - - def delete(self, url, headers=None, body=None, extra_headers=False): - """Send a HTTP DELETE request using keystone service catalog and auth - - :param str url: the relative url to send the post request to - :param dict headers: The headers to use for the request - :param dict body: the request body - :param dict extra_headers: If the headers returned by the get_headers() - method are to be used but additional headers - are needed in the request pass them in as a - dict - :return: a tuple with the first entry containing the response headers - and the second the response body - :rtype: tuple - """ - return self.request('DELETE', url, extra_headers, headers, body) - - def patch(self, url, body, headers=None, extra_headers=False): - """Send a HTTP PATCH request using keystone service catalog and auth - - :param str url: the relative url to send the post request to - :param dict body: the request body - :param dict headers: The headers to use for the request - :param dict extra_headers: If the headers returned by the get_headers() - method are to be used but additional headers - are needed in the request pass them in as a - dict - :return: a tuple with the first entry containing the response headers - and the second the response body - :rtype: tuple - """ - return self.request('PATCH', url, extra_headers, headers, body) - - def put(self, url, body, headers=None, extra_headers=False): - """Send a HTTP PUT request using keystone service catalog and auth - - :param str url: the relative url to send the post request to - :param dict body: the request body - :param dict headers: The headers to use for the request - :param dict extra_headers: If the headers returned by the get_headers() - method are to be used but additional headers - are needed in the request pass them in as a - dict - :return: a tuple with the first entry containing the response headers - and the second the response body - :rtype: tuple - """ - return self.request('PUT', url, extra_headers, headers, body) - - def head(self, url, headers=None, extra_headers=False): - """Send a HTTP HEAD request using keystone service catalog and auth - - :param str url: the relative url to send the post request to - :param dict headers: The headers to use for the request - :param dict extra_headers: If the headers returned by the get_headers() - method are to be used but additional headers - are needed in the request pass them in as a - dict - :return: a tuple with the first entry containing the response headers - and the second the response body - :rtype: tuple - """ - return self.request('HEAD', url, extra_headers, headers) - - def copy(self, url, headers=None, extra_headers=False): - """Send a HTTP COPY request using keystone service catalog and auth - - :param str url: the relative url to send the post request to - :param dict headers: The headers to use for the request - :param dict extra_headers: If the headers returned by the get_headers() - method are to be used but additional headers - are needed in the request pass them in as a - dict - :return: a tuple with the first entry containing the response headers - and the second the response body - :rtype: tuple - """ - return self.request('COPY', url, extra_headers, headers) - - def get_versions(self): - """Get the versions on a endpoint from the keystone catalog - - This method will make a GET request on the baseurl from the keystone - catalog to return a list of API versions. It is expected that a GET - on the endpoint in the catalog will return a list of supported API - versions. - - :return tuple with response headers and list of version numbers - :rtype: tuple - """ - resp, body = self.get('') - body = self._parse_resp(body) - versions = map(lambda x: x['id'], body) - return resp, versions - - def _get_request_id(self, resp): - for i in ('x-openstack-request-id', 'x-compute-request-id'): - if i in resp: - return resp[i] - return "" - - def _safe_body(self, body, maxlen=4096): - # convert a structure into a string safely - try: - text = six.text_type(body) - except UnicodeDecodeError: - # if this isn't actually text, return marker that - return "<BinaryData: removed>" - if len(text) > maxlen: - return text[:maxlen] - else: - return text - - def _log_request_start(self, method, req_url, req_headers=None, - req_body=None): - if req_headers is None: - req_headers = {} - caller_name = misc_utils.find_test_caller() - if self.trace_requests and re.search(self.trace_requests, caller_name): - self.LOG.debug('Starting Request (%s): %s %s' % - (caller_name, method, req_url)) - - def _log_request_full(self, method, req_url, resp, - secs="", req_headers=None, - req_body=None, resp_body=None, - caller_name=None, extra=None): - if 'X-Auth-Token' in req_headers: - req_headers['X-Auth-Token'] = '<omitted>' - log_fmt = """Request - Headers: %s - Body: %s - Response - Headers: %s - Body: %s""" - - self.LOG.debug( - log_fmt % ( - str(req_headers), - self._safe_body(req_body), - str(resp), - self._safe_body(resp_body)), - extra=extra) - - def _log_request(self, method, req_url, resp, - secs="", req_headers=None, - req_body=None, resp_body=None): - if req_headers is None: - req_headers = {} - # if we have the request id, put it in the right part of the log - extra = dict(request_id=self._get_request_id(resp)) - # NOTE(sdague): while we still have 6 callers to this function - # we're going to just provide work around on who is actually - # providing timings by gracefully adding no content if they don't. - # Once we're down to 1 caller, clean this up. - caller_name = misc_utils.find_test_caller() - if secs: - secs = " %.3fs" % secs - self.LOG.info( - 'Request (%s): %s %s %s%s' % ( - caller_name, - resp['status'], - method, - req_url, - secs), - extra=extra) - - # Also look everything at DEBUG if you want to filter this - # out, don't run at debug. - if self.LOG.isEnabledFor(real_logging.DEBUG): - self._log_request_full(method, req_url, resp, secs, req_headers, - req_body, resp_body, caller_name, extra) - - def _parse_resp(self, body): - try: - body = json.loads(body) - except ValueError: - return body - - # We assume, that if the first value of the deserialized body's - # item set is a dict or a list, that we just return the first value - # of deserialized body. - # Essentially "cutting out" the first placeholder element in a body - # that looks like this: - # - # { - # "users": [ - # ... - # ] - # } - try: - # Ensure there are not more than one top-level keys - # NOTE(freerunner): Ensure, that JSON is not nullable to - # to prevent StopIteration Exception - if len(body.keys()) != 1: - return body - # Just return the "wrapped" element - first_key, first_item = six.next(six.iteritems(body)) - if isinstance(first_item, (dict, list)): - return first_item - except (ValueError, IndexError): - pass - return body - - def response_checker(self, method, resp, resp_body): - """A sanity check on the response from a HTTP request - - This method does a sanity check on whether the response from an HTTP - request conforms the HTTP RFC. - - :param str method: The HTTP verb of the request associated with the - response being passed in. - :param resp: The response headers - :param resp_body: The body of the response - :raises ResponseWithNonEmptyBody: If the response with the status code - is not supposed to have a body - :raises ResponseWithEntity: If the response code is 205 but has an - entity - """ - if (resp.status in set((204, 205, 304)) or resp.status < 200 or - method.upper() == 'HEAD') and resp_body: - raise exceptions.ResponseWithNonEmptyBody(status=resp.status) - # NOTE(afazekas): - # If the HTTP Status Code is 205 - # 'The response MUST NOT include an entity.' - # A HTTP entity has an entity-body and an 'entity-header'. - # In the HTTP response specification (Section 6) the 'entity-header' - # 'generic-header' and 'response-header' are in OR relation. - # All headers not in the above two group are considered as entity - # header in every interpretation. - - if (resp.status == 205 and - 0 != len(set(resp.keys()) - set(('status',)) - - self.response_header_lc - self.general_header_lc)): - raise exceptions.ResponseWithEntity() - # NOTE(afazekas) - # Now the swift sometimes (delete not empty container) - # returns with non json error response, we can create new rest class - # for swift. - # Usually RFC2616 says error responses SHOULD contain an explanation. - # The warning is normal for SHOULD/SHOULD NOT case - - # Likely it will cause an error - if method != 'HEAD' and not resp_body and resp.status >= 400: - self.LOG.warning("status >= 400 response with empty body") - - def _request(self, method, url, headers=None, body=None): - """A simple HTTP request interface.""" - # Authenticate the request with the auth provider - req_url, req_headers, req_body = self.auth_provider.auth_request( - method, url, headers, body, self.filters) - - # Do the actual request, and time it - start = time.time() - self._log_request_start(method, req_url) - resp, resp_body = self.raw_request( - req_url, method, headers=req_headers, body=req_body) - end = time.time() - self._log_request(method, req_url, resp, secs=(end - start), - req_headers=req_headers, req_body=req_body, - resp_body=resp_body) - - # Verify HTTP response codes - self.response_checker(method, resp, resp_body) - - return resp, resp_body - - def raw_request(self, url, method, headers=None, body=None): - """Send a raw HTTP request without the keystone catalog or auth - - This method sends a HTTP request in the same manner as the request() - method, however it does so without using keystone auth or the catalog - to determine the base url. Additionally no response handling is done - the results from the request are just returned. - - :param str url: Full url to send the request - :param str method: The HTTP verb to use for the request - :param str headers: Headers to use for the request if none are specifed - the headers - :param str body: Body to to send with the request - :rtype: tuple - :return: a tuple with the first entry containing the response headers - and the second the response body - """ - if headers is None: - headers = self.get_headers() - return self.http_obj.request(url, method, - headers=headers, body=body) - - def request(self, method, url, extra_headers=False, headers=None, - body=None): - """Send a HTTP request with keystone auth and using the catalog - - This method will send an HTTP request using keystone auth in the - headers and the catalog to determine the endpoint to use for the - baseurl to send the request to. Additionally - - When a response is received it will check it to see if an error - response was received. If it was an exception will be raised to enable - it to be handled quickly. - - This method will also handle rate-limiting, if a 413 response code is - received it will retry the request after waiting the 'retry-after' - duration from the header. - - :param str method: The HTTP verb to use for the request - :param str url: Relative url to send the request to - :param dict extra_headers: If specified without the headers kwarg the - headers sent with the request will be the - combination from the get_headers() method - and this kwarg - :param dict headers: Headers to use for the request if none are - specifed the headers returned from the - get_headers() method are used. If the request - explicitly requires no headers use an empty dict. - :param str body: Body to to send with the request - :rtype: tuple - :return: a tuple with the first entry containing the response headers - and the second the response body - :raises UnexpectedContentType: If the content-type of the response - isn't an expect type - :raises Unauthorized: If a 401 response code is received - :raises Forbidden: If a 403 response code is received - :raises NotFound: If a 404 response code is received - :raises BadRequest: If a 400 response code is received - :raises Gone: If a 410 response code is received - :raises Conflict: If a 409 response code is received - :raises OverLimit: If a 413 response code is received and over_limit is - not in the response body - :raises RateLimitExceeded: If a 413 response code is received and - over_limit is in the response body - :raises InvalidContentType: If a 415 response code is received - :raises UnprocessableEntity: If a 422 response code is received - :raises InvalidHTTPResponseBody: The response body wasn't valid JSON - and couldn't be parsed - :raises NotImplemented: If a 501 response code is received - :raises ServerFault: If a 500 response code is received - :raises UnexpectedResponseCode: If a response code above 400 is - received and it doesn't fall into any - of the handled checks - """ - # if extra_headers is True - # default headers would be added to headers - retry = 0 - - if headers is None: - # NOTE(vponomaryov): if some client do not need headers, - # it should explicitly pass empty dict - headers = self.get_headers() - elif extra_headers: - try: - headers = headers.copy() - headers.update(self.get_headers()) - except (ValueError, TypeError): - headers = self.get_headers() - - resp, resp_body = self._request(method, url, - headers=headers, body=body) - - while (resp.status == 413 and - 'retry-after' in resp and - not self.is_absolute_limit( - resp, self._parse_resp(resp_body)) and - retry < MAX_RECURSION_DEPTH): - retry += 1 - delay = int(resp['retry-after']) - time.sleep(delay) - resp, resp_body = self._request(method, url, - headers=headers, body=body) - self._error_checker(method, url, headers, body, - resp, resp_body) - return resp, resp_body - - def _error_checker(self, method, url, - headers, body, resp, resp_body): - - # NOTE(mtreinish): Check for httplib response from glance_http. The - # object can't be used here because importing httplib breaks httplib2. - # If another object from a class not imported were passed here as - # resp this could possibly fail - if str(type(resp)) == "<type 'instance'>": - ctype = resp.getheader('content-type') - else: - try: - ctype = resp['content-type'] - # NOTE(mtreinish): Keystone delete user responses doesn't have a - # content-type header. (They don't have a body) So just pretend it - # is set. - except KeyError: - ctype = 'application/json' - - # It is not an error response - if resp.status < 400: - return - - JSON_ENC = ['application/json', 'application/json; charset=utf-8'] - # NOTE(mtreinish): This is for compatibility with Glance and swift - # APIs. These are the return content types that Glance api v1 - # (and occasionally swift) are using. - TXT_ENC = ['text/plain', 'text/html', 'text/html; charset=utf-8', - 'text/plain; charset=utf-8'] - - if ctype.lower() in JSON_ENC: - parse_resp = True - elif ctype.lower() in TXT_ENC: - parse_resp = False - else: - raise exceptions.UnexpectedContentType(str(resp.status), - resp=resp) - - if resp.status == 401: - if parse_resp: - resp_body = self._parse_resp(resp_body) - raise exceptions.Unauthorized(resp_body, resp=resp) - - if resp.status == 403: - if parse_resp: - resp_body = self._parse_resp(resp_body) - raise exceptions.Forbidden(resp_body, resp=resp) - - if resp.status == 404: - if parse_resp: - resp_body = self._parse_resp(resp_body) - raise exceptions.NotFound(resp_body, resp=resp) - - if resp.status == 400: - if parse_resp: - resp_body = self._parse_resp(resp_body) - raise exceptions.BadRequest(resp_body, resp=resp) - - if resp.status == 410: - if parse_resp: - resp_body = self._parse_resp(resp_body) - raise exceptions.Gone(resp_body, resp=resp) - - if resp.status == 409: - if parse_resp: - resp_body = self._parse_resp(resp_body) - raise exceptions.Conflict(resp_body, resp=resp) - - if resp.status == 413: - if parse_resp: - resp_body = self._parse_resp(resp_body) - if self.is_absolute_limit(resp, resp_body): - raise exceptions.OverLimit(resp_body, resp=resp) - else: - raise exceptions.RateLimitExceeded(resp_body, resp=resp) - - if resp.status == 415: - if parse_resp: - resp_body = self._parse_resp(resp_body) - raise exceptions.InvalidContentType(resp_body, resp=resp) - - if resp.status == 422: - if parse_resp: - resp_body = self._parse_resp(resp_body) - raise exceptions.UnprocessableEntity(resp_body, resp=resp) - - if resp.status in (500, 501): - message = resp_body - if parse_resp: - try: - resp_body = self._parse_resp(resp_body) - except ValueError: - # If response body is a non-json string message. - # Use resp_body as is and raise InvalidResponseBody - # exception. - raise exceptions.InvalidHTTPResponseBody(message) - else: - if isinstance(resp_body, dict): - # I'm seeing both computeFault - # and cloudServersFault come back. - # Will file a bug to fix, but leave as is for now. - if 'cloudServersFault' in resp_body: - message = resp_body['cloudServersFault']['message'] - elif 'computeFault' in resp_body: - message = resp_body['computeFault']['message'] - elif 'error' in resp_body: - message = resp_body['error']['message'] - elif 'message' in resp_body: - message = resp_body['message'] - else: - message = resp_body - - if resp.status == 501: - raise exceptions.NotImplemented(resp_body, resp=resp, - message=message) - else: - raise exceptions.ServerFault(resp_body, resp=resp, - message=message) - - if resp.status >= 400: - raise exceptions.UnexpectedResponseCode(str(resp.status), - resp=resp) - - def is_absolute_limit(self, resp, resp_body): - if (not isinstance(resp_body, collections.Mapping) or - 'retry-after' not in resp): - return True - over_limit = resp_body.get('overLimit', None) - if not over_limit: - return True - return 'exceed' in over_limit.get('message', 'blabla') - - def wait_for_resource_deletion(self, id): - """Waits for a resource to be deleted - - This method will loop over is_resource_deleted until either - is_resource_deleted returns True or the build timeout is reached. This - depends on is_resource_deleted being implemented - - :param str id: The id of the resource to check - :raises TimeoutException: If the build_timeout has elapsed and the - resource still hasn't been deleted - """ - start_time = int(time.time()) - while True: - if self.is_resource_deleted(id): - return - if int(time.time()) - start_time >= self.build_timeout: - message = ('Failed to delete %(resource_type)s %(id)s within ' - 'the required time (%(timeout)s s).' % - {'resource_type': self.resource_type, 'id': id, - 'timeout': self.build_timeout}) - caller = misc_utils.find_test_caller() - if caller: - message = '(%s) %s' % (caller, message) - raise exceptions.TimeoutException(message) - time.sleep(self.build_interval) - - def is_resource_deleted(self, id): - """Subclasses override with specific deletion detection.""" - message = ('"%s" does not implement is_resource_deleted' - % self.__class__.__name__) - raise NotImplementedError(message) - - @property - def resource_type(self): - """Returns the primary type of resource this client works with.""" - return 'resource' - - @classmethod - def validate_response(cls, schema, resp, body): - # Only check the response if the status code is a success code - # TODO(cyeoh): Eventually we should be able to verify that a failure - # code if it exists is something that we expect. This is explicitly - # declared in the V3 API and so we should be able to export this in - # the response schema. For now we'll ignore it. - if resp.status in HTTP_SUCCESS + HTTP_REDIRECTION: - cls.expected_success(schema['status_code'], resp.status) - - # Check the body of a response - body_schema = schema.get('response_body') - if body_schema: - try: - jsonschema.validate(body, body_schema, - cls=JSONSCHEMA_VALIDATOR, - format_checker=FORMAT_CHECKER) - except jsonschema.ValidationError as ex: - msg = ("HTTP response body is invalid (%s)") % ex - raise exceptions.InvalidHTTPResponseBody(msg) - else: - if body: - msg = ("HTTP response body should not exist (%s)") % body - raise exceptions.InvalidHTTPResponseBody(msg) - - # Check the header of a response - header_schema = schema.get('response_header') - if header_schema: - try: - jsonschema.validate(resp, header_schema, - cls=JSONSCHEMA_VALIDATOR, - format_checker=FORMAT_CHECKER) - except jsonschema.ValidationError as ex: - msg = ("HTTP response header is invalid (%s)") % ex - raise exceptions.InvalidHTTPResponseHeader(msg) - - -class ResponseBody(dict): - """Class that wraps an http response and dict body into a single value. - - Callers that receive this object will normally use it as a dict but - can extract the response if needed. - """ - - def __init__(self, response, body=None): - body_data = body or {} - self.update(body_data) - self.response = response - - def __str__(self): - body = super(ResponseBody, self).__str__() - return "response: %s\nBody: %s" % (self.response, body) - - -class ResponseBodyData(object): - """Class that wraps an http response and string data into a single value. - - """ - - def __init__(self, response, data): - self.response = response - self.data = data - - def __str__(self): - return "response: %s\nBody: %s" % (self.response, self.data) - - -class ResponseBodyList(list): - """Class that wraps an http response and list body into a single value. - - Callers that receive this object will normally use it as a list but - can extract the response if needed. - """ - - def __init__(self, response, body=None): - body_data = body or [] - self.extend(body_data) - self.response = response - - def __str__(self): - body = super(ResponseBodyList, self).__str__() - return "response: %s\nBody: %s" % (self.response, body) |