summaryrefslogtreecommitdiff
path: root/tempest_lib/common/rest_client.py
diff options
context:
space:
mode:
Diffstat (limited to 'tempest_lib/common/rest_client.py')
-rw-r--r--tempest_lib/common/rest_client.py894
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)