887 lines
		
	
	
		
			37 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			887 lines
		
	
	
		
			37 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# 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
 | 
						|
            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 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 == 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)
 |