blob: f5bff20f60b11bd96a55691e491663d12951fbdc [file] [log] [blame]
Matthew Treinish9e26ca82016-02-23 11:43:20 -05001# Copyright 2012 OpenStack Foundation
2# Copyright 2013 IBM Corp.
3# All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
17import collections
Paul Glass119565a2016-04-06 11:41:42 -050018import email.utils
Matthew Treinish9e26ca82016-02-23 11:43:20 -050019import re
20import time
21
22import jsonschema
23from oslo_log import log as logging
24from oslo_serialization import jsonutils as json
25import six
26
27from tempest.lib.common import http
ghanshyamf9ded352016-04-12 17:03:01 +090028from tempest.lib.common import jsonschema_validator
Jordan Pittier9e227c52016-02-09 14:35:18 +010029from tempest.lib.common.utils import test_utils
Matthew Treinish9e26ca82016-02-23 11:43:20 -050030from tempest.lib import exceptions
31
32# redrive rate limited calls at most twice
33MAX_RECURSION_DEPTH = 2
34
35# All the successful HTTP status codes from RFC 7231 & 4918
36HTTP_SUCCESS = (200, 201, 202, 203, 204, 205, 206, 207)
37
38# All the redirection HTTP status codes from RFC 7231 & 4918
39HTTP_REDIRECTION = (300, 301, 302, 303, 304, 305, 306, 307)
40
41# JSON Schema validator and format checker used for JSON Schema validation
ghanshyamf9ded352016-04-12 17:03:01 +090042JSONSCHEMA_VALIDATOR = jsonschema_validator.JSONSCHEMA_VALIDATOR
43FORMAT_CHECKER = jsonschema_validator.FORMAT_CHECKER
Matthew Treinish9e26ca82016-02-23 11:43:20 -050044
45
46class RestClient(object):
47 """Unified OpenStack RestClient class
48
49 This class is used for building openstack api clients on top of. It is
50 intended to provide a base layer for wrapping outgoing http requests in
51 keystone auth as well as providing response code checking and error
52 handling.
53
54 :param auth_provider: an auth provider object used to wrap requests in auth
55 :param str service: The service name to use for the catalog lookup
56 :param str region: The region to use for the catalog lookup
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -060057 :param str name: The endpoint name to use for the catalog lookup; this
58 returns only if the service exists
Matthew Treinish9e26ca82016-02-23 11:43:20 -050059 :param str endpoint_type: The endpoint type to use for the catalog lookup
60 :param int build_interval: Time in seconds between to status checks in
61 wait loops
62 :param int build_timeout: Timeout in seconds to wait for a wait operation.
63 :param bool disable_ssl_certificate_validation: Set to true to disable ssl
64 certificate validation
65 :param str ca_certs: File containing the CA Bundle to use in verifying a
66 TLS server cert
guo yunxian6f24cc42016-07-29 20:03:41 +080067 :param str trace_requests: Regex to use for specifying logging the entirety
Matthew Treinish9e26ca82016-02-23 11:43:20 -050068 of the request and response payload
zhufl071e94c2016-07-12 10:26:34 +080069 :param str http_timeout: Timeout in seconds to wait for the http request to
70 return
Matthew Treinish9e26ca82016-02-23 11:43:20 -050071 """
72 TYPE = "json"
73
74 # The version of the API this client implements
75 api_version = None
76
77 LOG = logging.getLogger(__name__)
78
79 def __init__(self, auth_provider, service, region,
80 endpoint_type='publicURL',
81 build_interval=1, build_timeout=60,
82 disable_ssl_certificate_validation=False, ca_certs=None,
zhufl071e94c2016-07-12 10:26:34 +080083 trace_requests='', name=None, http_timeout=None):
Matthew Treinish9e26ca82016-02-23 11:43:20 -050084 self.auth_provider = auth_provider
85 self.service = service
86 self.region = region
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -060087 self.name = name
Matthew Treinish9e26ca82016-02-23 11:43:20 -050088 self.endpoint_type = endpoint_type
89 self.build_interval = build_interval
90 self.build_timeout = build_timeout
91 self.trace_requests = trace_requests
92
93 self._skip_path = False
94 self.general_header_lc = set(('cache-control', 'connection',
95 'date', 'pragma', 'trailer',
96 'transfer-encoding', 'via',
97 'warning'))
98 self.response_header_lc = set(('accept-ranges', 'age', 'etag',
99 'location', 'proxy-authenticate',
100 'retry-after', 'server',
101 'vary', 'www-authenticate'))
102 dscv = disable_ssl_certificate_validation
103 self.http_obj = http.ClosingHttp(
zhufl071e94c2016-07-12 10:26:34 +0800104 disable_ssl_certificate_validation=dscv, ca_certs=ca_certs,
105 timeout=http_timeout)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500106
107 def _get_type(self):
guo yunxiana2216472016-08-01 16:34:43 +0800108 if self.TYPE != "json":
109 self.LOG.warning("Tempest has dropped XML support and the TYPE "
110 "became meaningless")
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500111 return self.TYPE
112
113 def get_headers(self, accept_type=None, send_type=None):
114 """Return the default headers which will be used with outgoing requests
115
116 :param str accept_type: The media type to use for the Accept header, if
117 one isn't provided the object var TYPE will be
118 used
119 :param str send_type: The media-type to use for the Content-Type
120 header, if one isn't provided the object var
121 TYPE will be used
122 :rtype: dict
123 :return: The dictionary of headers which can be used in the headers
124 dict for outgoing request
125 """
126 if accept_type is None:
127 accept_type = self._get_type()
128 if send_type is None:
129 send_type = self._get_type()
130 return {'Content-Type': 'application/%s' % send_type,
131 'Accept': 'application/%s' % accept_type}
132
133 def __str__(self):
134 STRING_LIMIT = 80
135 str_format = ("service:%s, base_url:%s, "
136 "filters: %s, build_interval:%s, build_timeout:%s"
137 "\ntoken:%s..., \nheaders:%s...")
138 return str_format % (self.service, self.base_url,
139 self.filters, self.build_interval,
140 self.build_timeout,
141 str(self.token)[0:STRING_LIMIT],
142 str(self.get_headers())[0:STRING_LIMIT])
143
144 @property
145 def user(self):
146 """The username used for requests
147
148 :rtype: string
149 :return: The username being used for requests
150 """
151
152 return self.auth_provider.credentials.username
153
154 @property
155 def user_id(self):
156 """The user_id used for requests
157
158 :rtype: string
159 :return: The user id being used for requests
160 """
161 return self.auth_provider.credentials.user_id
162
163 @property
164 def tenant_name(self):
165 """The tenant/project being used for requests
166
167 :rtype: string
168 :return: The tenant/project name being used for requests
169 """
170 return self.auth_provider.credentials.tenant_name
171
172 @property
173 def tenant_id(self):
174 """The tenant/project id being used for requests
175
176 :rtype: string
177 :return: The tenant/project id being used for requests
178 """
179 return self.auth_provider.credentials.tenant_id
180
181 @property
182 def password(self):
183 """The password being used for requests
184
185 :rtype: string
186 :return: The password being used for requests
187 """
188 return self.auth_provider.credentials.password
189
190 @property
191 def base_url(self):
192 return self.auth_provider.base_url(filters=self.filters)
193
194 @property
195 def token(self):
196 return self.auth_provider.get_token()
197
198 @property
199 def filters(self):
200 _filters = dict(
201 service=self.service,
202 endpoint_type=self.endpoint_type,
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -0600203 region=self.region,
204 name=self.name
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500205 )
206 if self.api_version is not None:
207 _filters['api_version'] = self.api_version
208 if self._skip_path:
209 _filters['skip_path'] = self._skip_path
210 return _filters
211
212 def skip_path(self):
213 """When set, ignore the path part of the base URL from the catalog"""
214 self._skip_path = True
215
216 def reset_path(self):
217 """When reset, use the base URL from the catalog as-is"""
218 self._skip_path = False
219
220 @classmethod
221 def expected_success(cls, expected_code, read_code):
222 """Check expected success response code against the http response
223
224 :param int expected_code: The response code that is expected.
225 Optionally a list of integers can be used
226 to specify multiple valid success codes
227 :param int read_code: The response code which was returned in the
228 response
229 :raises AssertionError: if the expected_code isn't a valid http success
230 response code
231 :raises exceptions.InvalidHttpSuccessCode: if the read code isn't an
232 expected http success code
233 """
ghanshyamc3074202016-04-18 15:20:45 +0900234 if not isinstance(read_code, int):
235 raise TypeError("'read_code' must be an int instead of (%s)"
236 % type(read_code))
237
Hanxi2f977db2016-09-01 17:31:28 +0800238 assert_msg = ("This function only allowed to use for HTTP status "
239 "codes which explicitly defined in the RFC 7231 & 4918. "
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500240 "{0} is not a defined Success Code!"
241 ).format(expected_code)
242 if isinstance(expected_code, list):
243 for code in expected_code:
244 assert code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg
245 else:
246 assert expected_code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg
247
248 # NOTE(afazekas): the http status code above 400 is processed by
249 # the _error_checker method
250 if read_code < 400:
zhufl4d2f5152017-01-17 11:16:12 +0800251 pattern = ("Unexpected http success status code {0}, "
252 "The expected status code is {1}")
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500253 if ((not isinstance(expected_code, list) and
254 (read_code != expected_code)) or
255 (isinstance(expected_code, list) and
256 (read_code not in expected_code))):
257 details = pattern.format(read_code, expected_code)
258 raise exceptions.InvalidHttpSuccessCode(details)
259
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200260 def post(self, url, body, headers=None, extra_headers=False,
261 chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500262 """Send a HTTP POST request using keystone auth
263
264 :param str url: the relative url to send the post request to
265 :param dict body: the request body
266 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300267 :param bool extra_headers: Boolean value than indicates if the headers
268 returned by the get_headers() method are to
269 be used but additional headers are needed in
270 the request pass them in as a dict.
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200271 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500272 :return: a tuple with the first entry containing the response headers
273 and the second the response body
274 :rtype: tuple
275 """
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200276 return self.request('POST', url, extra_headers, headers, body, chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500277
278 def get(self, url, headers=None, extra_headers=False):
279 """Send a HTTP GET request using keystone service catalog and auth
280
281 :param str url: the relative url to send the post request to
282 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300283 :param bool extra_headers: Boolean value than indicates if the headers
284 returned by the get_headers() method are to
285 be used but additional headers are needed in
286 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500287 :return: a tuple with the first entry containing the response headers
288 and the second the response body
289 :rtype: tuple
290 """
291 return self.request('GET', url, extra_headers, headers)
292
293 def delete(self, url, headers=None, body=None, extra_headers=False):
294 """Send a HTTP DELETE request using keystone service catalog and auth
295
296 :param str url: the relative url to send the post request to
297 :param dict headers: The headers to use for the request
298 :param dict body: the request body
vsaienko9eb846b2016-04-09 00:35:47 +0300299 :param bool extra_headers: Boolean value than indicates if the headers
300 returned by the get_headers() method are to
301 be used but additional headers are needed in
302 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500303 :return: a tuple with the first entry containing the response headers
304 and the second the response body
305 :rtype: tuple
306 """
307 return self.request('DELETE', url, extra_headers, headers, body)
308
309 def patch(self, url, body, headers=None, extra_headers=False):
310 """Send a HTTP PATCH request using keystone service catalog and auth
311
312 :param str url: the relative url to send the post request to
313 :param dict body: the request body
314 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300315 :param bool extra_headers: Boolean value than indicates if the headers
316 returned by the get_headers() method are to
317 be used but additional headers are needed in
318 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500319 :return: a tuple with the first entry containing the response headers
320 and the second the response body
321 :rtype: tuple
322 """
323 return self.request('PATCH', url, extra_headers, headers, body)
324
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200325 def put(self, url, body, headers=None, extra_headers=False, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500326 """Send a HTTP PUT request using keystone service catalog and auth
327
328 :param str url: the relative url to send the post request to
329 :param dict body: the request body
330 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300331 :param bool extra_headers: Boolean value than indicates if the headers
332 returned by the get_headers() method are to
333 be used but additional headers are needed in
334 the request pass them in as a dict.
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200335 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500336 :return: a tuple with the first entry containing the response headers
337 and the second the response body
338 :rtype: tuple
339 """
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200340 return self.request('PUT', url, extra_headers, headers, body, chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500341
342 def head(self, url, headers=None, extra_headers=False):
343 """Send a HTTP HEAD request using keystone service catalog and auth
344
345 :param str url: the relative url to send the post request to
346 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300347 :param bool extra_headers: Boolean value than indicates if the headers
348 returned by the get_headers() method are to
349 be used but additional headers are needed in
350 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500351 :return: a tuple with the first entry containing the response headers
352 and the second the response body
353 :rtype: tuple
354 """
355 return self.request('HEAD', url, extra_headers, headers)
356
357 def copy(self, url, headers=None, extra_headers=False):
358 """Send a HTTP COPY request using keystone service catalog and auth
359
360 :param str url: the relative url to send the post request to
361 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300362 :param bool extra_headers: Boolean value than indicates if the headers
363 returned by the get_headers() method are to
364 be used but additional headers are needed in
365 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500366 :return: a tuple with the first entry containing the response headers
367 and the second the response body
368 :rtype: tuple
369 """
370 return self.request('COPY', url, extra_headers, headers)
371
372 def get_versions(self):
373 """Get the versions on a endpoint from the keystone catalog
374
375 This method will make a GET request on the baseurl from the keystone
376 catalog to return a list of API versions. It is expected that a GET
377 on the endpoint in the catalog will return a list of supported API
378 versions.
379
380 :return tuple with response headers and list of version numbers
381 :rtype: tuple
382 """
383 resp, body = self.get('')
384 body = self._parse_resp(body)
385 versions = map(lambda x: x['id'], body)
386 return resp, versions
387
388 def _get_request_id(self, resp):
389 for i in ('x-openstack-request-id', 'x-compute-request-id'):
390 if i in resp:
391 return resp[i]
392 return ""
393
394 def _safe_body(self, body, maxlen=4096):
395 # convert a structure into a string safely
396 try:
397 text = six.text_type(body)
398 except UnicodeDecodeError:
399 # if this isn't actually text, return marker that
400 return "<BinaryData: removed>"
401 if len(text) > maxlen:
402 return text[:maxlen]
403 else:
404 return text
405
guo yunxian9f749f92016-08-25 10:55:04 +0800406 def _log_request_start(self, method, req_url):
Jordan Pittier9e227c52016-02-09 14:35:18 +0100407 caller_name = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500408 if self.trace_requests and re.search(self.trace_requests, caller_name):
Jordan Pittier525ec712016-12-07 17:51:26 +0100409 self.LOG.debug('Starting Request (%s): %s %s', caller_name,
410 method, req_url)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500411
guo yunxian9f749f92016-08-25 10:55:04 +0800412 def _log_request_full(self, resp, req_headers=None, req_body=None,
413 resp_body=None, extra=None):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500414 if 'X-Auth-Token' in req_headers:
415 req_headers['X-Auth-Token'] = '<omitted>'
Andrea Frittoli (andreaf)a1edb2d2016-05-10 16:09:59 +0100416 # A shallow copy is sufficient
417 resp_log = resp.copy()
418 if 'x-subject-token' in resp_log:
419 resp_log['x-subject-token'] = '<omitted>'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500420 log_fmt = """Request - Headers: %s
421 Body: %s
422 Response - Headers: %s
423 Body: %s"""
424
425 self.LOG.debug(
Jordan Pittier525ec712016-12-07 17:51:26 +0100426 log_fmt,
427 str(req_headers),
428 self._safe_body(req_body),
429 str(resp_log),
430 self._safe_body(resp_body),
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500431 extra=extra)
432
433 def _log_request(self, method, req_url, resp,
434 secs="", req_headers=None,
435 req_body=None, resp_body=None):
436 if req_headers is None:
437 req_headers = {}
438 # if we have the request id, put it in the right part of the log
439 extra = dict(request_id=self._get_request_id(resp))
440 # NOTE(sdague): while we still have 6 callers to this function
441 # we're going to just provide work around on who is actually
442 # providing timings by gracefully adding no content if they don't.
443 # Once we're down to 1 caller, clean this up.
Jordan Pittier9e227c52016-02-09 14:35:18 +0100444 caller_name = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500445 if secs:
446 secs = " %.3fs" % secs
447 self.LOG.info(
Jordan Pittier525ec712016-12-07 17:51:26 +0100448 'Request (%s): %s %s %s%s',
449 caller_name,
450 resp['status'],
451 method,
452 req_url,
453 secs,
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500454 extra=extra)
455
456 # Also look everything at DEBUG if you want to filter this
457 # out, don't run at debug.
Anusha Raminenif3eb9472017-01-13 08:54:01 +0530458 if self.LOG.isEnabledFor(logging.DEBUG):
guo yunxian9f749f92016-08-25 10:55:04 +0800459 self._log_request_full(resp, req_headers, req_body,
460 resp_body, extra)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500461
462 def _parse_resp(self, body):
463 try:
464 body = json.loads(body)
465 except ValueError:
466 return body
467
468 # We assume, that if the first value of the deserialized body's
469 # item set is a dict or a list, that we just return the first value
470 # of deserialized body.
471 # Essentially "cutting out" the first placeholder element in a body
472 # that looks like this:
473 #
474 # {
475 # "users": [
476 # ...
477 # ]
478 # }
479 try:
480 # Ensure there are not more than one top-level keys
481 # NOTE(freerunner): Ensure, that JSON is not nullable to
482 # to prevent StopIteration Exception
483 if len(body.keys()) != 1:
484 return body
485 # Just return the "wrapped" element
486 first_key, first_item = six.next(six.iteritems(body))
487 if isinstance(first_item, (dict, list)):
488 return first_item
489 except (ValueError, IndexError):
490 pass
491 return body
492
493 def response_checker(self, method, resp, resp_body):
494 """A sanity check on the response from a HTTP request
495
496 This method does a sanity check on whether the response from an HTTP
497 request conforms the HTTP RFC.
498
499 :param str method: The HTTP verb of the request associated with the
500 response being passed in.
501 :param resp: The response headers
502 :param resp_body: The body of the response
503 :raises ResponseWithNonEmptyBody: If the response with the status code
504 is not supposed to have a body
505 :raises ResponseWithEntity: If the response code is 205 but has an
506 entity
507 """
508 if (resp.status in set((204, 205, 304)) or resp.status < 200 or
509 method.upper() == 'HEAD') and resp_body:
510 raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
511 # NOTE(afazekas):
512 # If the HTTP Status Code is 205
513 # 'The response MUST NOT include an entity.'
514 # A HTTP entity has an entity-body and an 'entity-header'.
515 # In the HTTP response specification (Section 6) the 'entity-header'
516 # 'generic-header' and 'response-header' are in OR relation.
517 # All headers not in the above two group are considered as entity
518 # header in every interpretation.
519
520 if (resp.status == 205 and
521 0 != len(set(resp.keys()) - set(('status',)) -
522 self.response_header_lc - self.general_header_lc)):
523 raise exceptions.ResponseWithEntity()
524 # NOTE(afazekas)
525 # Now the swift sometimes (delete not empty container)
526 # returns with non json error response, we can create new rest class
527 # for swift.
528 # Usually RFC2616 says error responses SHOULD contain an explanation.
529 # The warning is normal for SHOULD/SHOULD NOT case
530
531 # Likely it will cause an error
532 if method != 'HEAD' and not resp_body and resp.status >= 400:
533 self.LOG.warning("status >= 400 response with empty body")
534
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200535 def _request(self, method, url, headers=None, body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500536 """A simple HTTP request interface."""
537 # Authenticate the request with the auth provider
538 req_url, req_headers, req_body = self.auth_provider.auth_request(
539 method, url, headers, body, self.filters)
540
541 # Do the actual request, and time it
542 start = time.time()
543 self._log_request_start(method, req_url)
544 resp, resp_body = self.raw_request(
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200545 req_url, method, headers=req_headers, body=req_body,
546 chunked=chunked
547 )
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500548 end = time.time()
549 self._log_request(method, req_url, resp, secs=(end - start),
550 req_headers=req_headers, req_body=req_body,
551 resp_body=resp_body)
552
553 # Verify HTTP response codes
554 self.response_checker(method, resp, resp_body)
555
556 return resp, resp_body
557
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200558 def raw_request(self, url, method, headers=None, body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500559 """Send a raw HTTP request without the keystone catalog or auth
560
561 This method sends a HTTP request in the same manner as the request()
562 method, however it does so without using keystone auth or the catalog
563 to determine the base url. Additionally no response handling is done
564 the results from the request are just returned.
565
566 :param str url: Full url to send the request
567 :param str method: The HTTP verb to use for the request
568 :param str headers: Headers to use for the request if none are specifed
569 the headers
Anh Trand44a8be2016-03-25 09:49:14 +0700570 :param str body: Body to send with the request
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200571 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500572 :rtype: tuple
573 :return: a tuple with the first entry containing the response headers
574 and the second the response body
575 """
576 if headers is None:
577 headers = self.get_headers()
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200578 return self.http_obj.request(url, method, headers=headers,
579 body=body, chunked=chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500580
581 def request(self, method, url, extra_headers=False, headers=None,
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200582 body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500583 """Send a HTTP request with keystone auth and using the catalog
584
585 This method will send an HTTP request using keystone auth in the
586 headers and the catalog to determine the endpoint to use for the
587 baseurl to send the request to. Additionally
588
589 When a response is received it will check it to see if an error
590 response was received. If it was an exception will be raised to enable
591 it to be handled quickly.
592
593 This method will also handle rate-limiting, if a 413 response code is
594 received it will retry the request after waiting the 'retry-after'
595 duration from the header.
596
597 :param str method: The HTTP verb to use for the request
598 :param str url: Relative url to send the request to
vsaienko9eb846b2016-04-09 00:35:47 +0300599 :param bool extra_headers: Boolean value than indicates if the headers
600 returned by the get_headers() method are to
601 be used but additional headers are needed in
602 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500603 :param dict headers: Headers to use for the request if none are
604 specifed the headers returned from the
605 get_headers() method are used. If the request
606 explicitly requires no headers use an empty dict.
Anh Trand44a8be2016-03-25 09:49:14 +0700607 :param str body: Body to send with the request
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200608 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500609 :rtype: tuple
610 :return: a tuple with the first entry containing the response headers
611 and the second the response body
612 :raises UnexpectedContentType: If the content-type of the response
613 isn't an expect type
614 :raises Unauthorized: If a 401 response code is received
615 :raises Forbidden: If a 403 response code is received
616 :raises NotFound: If a 404 response code is received
617 :raises BadRequest: If a 400 response code is received
618 :raises Gone: If a 410 response code is received
619 :raises Conflict: If a 409 response code is received
Kevin Bentona82bc862017-02-13 01:16:13 -0800620 :raises PreconditionFailed: If a 412 response code is received
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500621 :raises OverLimit: If a 413 response code is received and over_limit is
622 not in the response body
623 :raises RateLimitExceeded: If a 413 response code is received and
624 over_limit is in the response body
625 :raises InvalidContentType: If a 415 response code is received
626 :raises UnprocessableEntity: If a 422 response code is received
627 :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
628 and couldn't be parsed
629 :raises NotImplemented: If a 501 response code is received
630 :raises ServerFault: If a 500 response code is received
631 :raises UnexpectedResponseCode: If a response code above 400 is
632 received and it doesn't fall into any
633 of the handled checks
634 """
635 # if extra_headers is True
636 # default headers would be added to headers
637 retry = 0
638
639 if headers is None:
640 # NOTE(vponomaryov): if some client do not need headers,
641 # it should explicitly pass empty dict
642 headers = self.get_headers()
643 elif extra_headers:
644 try:
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500645 headers.update(self.get_headers())
646 except (ValueError, TypeError):
647 headers = self.get_headers()
648
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200649 resp, resp_body = self._request(method, url, headers=headers,
650 body=body, chunked=chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500651
652 while (resp.status == 413 and
653 'retry-after' in resp and
654 not self.is_absolute_limit(
655 resp, self._parse_resp(resp_body)) and
656 retry < MAX_RECURSION_DEPTH):
657 retry += 1
Paul Glass119565a2016-04-06 11:41:42 -0500658 delay = self._get_retry_after_delay(resp)
659 self.LOG.debug(
660 "Sleeping %s seconds based on retry-after header", delay
661 )
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500662 time.sleep(delay)
663 resp, resp_body = self._request(method, url,
664 headers=headers, body=body)
Ken'ichi Ohmichi17051e82016-10-20 11:46:06 -0700665 self._error_checker(resp, resp_body)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500666 return resp, resp_body
667
Paul Glass119565a2016-04-06 11:41:42 -0500668 def _get_retry_after_delay(self, resp):
669 """Extract the delay from the retry-after header.
670
671 This supports both integer and HTTP date formatted retry-after headers
672 per RFC 2616.
673
674 :param resp: The response containing the retry-after headers
675 :rtype: int
676 :return: The delay in seconds, clamped to be at least 1 second
677 :raises ValueError: On failing to parse the delay
678 """
679 delay = None
680 try:
681 delay = int(resp['retry-after'])
682 except (ValueError, KeyError):
683 pass
684
685 try:
686 retry_timestamp = self._parse_http_date(resp['retry-after'])
687 date_timestamp = self._parse_http_date(resp['date'])
688 delay = int(retry_timestamp - date_timestamp)
689 except (ValueError, OverflowError, KeyError):
690 pass
691
692 if delay is None:
693 raise ValueError(
694 "Failed to parse retry-after header %r as either int or "
695 "HTTP-date." % resp.get('retry-after')
696 )
697
698 # Retry-after headers do not have sub-second precision. Clients may
699 # receive a delay of 0. After sleeping 0 seconds, we would (likely) hit
700 # another 413. To avoid this, always sleep at least 1 second.
701 return max(1, delay)
702
703 def _parse_http_date(self, val):
704 """Parse an HTTP date, like 'Fri, 31 Dec 1999 23:59:59 GMT'.
705
706 Return an epoch timestamp (float), as returned by time.mktime().
707 """
708 parts = email.utils.parsedate(val)
709 if not parts:
710 raise ValueError("Failed to parse date %s" % val)
711 return time.mktime(parts)
712
Ken'ichi Ohmichi17051e82016-10-20 11:46:06 -0700713 def _error_checker(self, resp, resp_body):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500714
715 # NOTE(mtreinish): Check for httplib response from glance_http. The
716 # object can't be used here because importing httplib breaks httplib2.
717 # If another object from a class not imported were passed here as
718 # resp this could possibly fail
719 if str(type(resp)) == "<type 'instance'>":
720 ctype = resp.getheader('content-type')
721 else:
722 try:
723 ctype = resp['content-type']
724 # NOTE(mtreinish): Keystone delete user responses doesn't have a
725 # content-type header. (They don't have a body) So just pretend it
726 # is set.
727 except KeyError:
728 ctype = 'application/json'
729
730 # It is not an error response
731 if resp.status < 400:
732 return
733
734 JSON_ENC = ['application/json', 'application/json; charset=utf-8']
735 # NOTE(mtreinish): This is for compatibility with Glance and swift
736 # APIs. These are the return content types that Glance api v1
737 # (and occasionally swift) are using.
738 TXT_ENC = ['text/plain', 'text/html', 'text/html; charset=utf-8',
739 'text/plain; charset=utf-8']
740
741 if ctype.lower() in JSON_ENC:
742 parse_resp = True
743 elif ctype.lower() in TXT_ENC:
744 parse_resp = False
745 else:
746 raise exceptions.UnexpectedContentType(str(resp.status),
747 resp=resp)
748
749 if resp.status == 401:
750 if parse_resp:
751 resp_body = self._parse_resp(resp_body)
752 raise exceptions.Unauthorized(resp_body, resp=resp)
753
754 if resp.status == 403:
755 if parse_resp:
756 resp_body = self._parse_resp(resp_body)
757 raise exceptions.Forbidden(resp_body, resp=resp)
758
759 if resp.status == 404:
760 if parse_resp:
761 resp_body = self._parse_resp(resp_body)
762 raise exceptions.NotFound(resp_body, resp=resp)
763
764 if resp.status == 400:
765 if parse_resp:
766 resp_body = self._parse_resp(resp_body)
767 raise exceptions.BadRequest(resp_body, resp=resp)
768
769 if resp.status == 410:
770 if parse_resp:
771 resp_body = self._parse_resp(resp_body)
772 raise exceptions.Gone(resp_body, resp=resp)
773
774 if resp.status == 409:
775 if parse_resp:
776 resp_body = self._parse_resp(resp_body)
777 raise exceptions.Conflict(resp_body, resp=resp)
778
Kevin Bentona82bc862017-02-13 01:16:13 -0800779 if resp.status == 412:
780 if parse_resp:
781 resp_body = self._parse_resp(resp_body)
782 raise exceptions.PreconditionFailed(resp_body, resp=resp)
783
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500784 if resp.status == 413:
785 if parse_resp:
786 resp_body = self._parse_resp(resp_body)
787 if self.is_absolute_limit(resp, resp_body):
788 raise exceptions.OverLimit(resp_body, resp=resp)
789 else:
790 raise exceptions.RateLimitExceeded(resp_body, resp=resp)
791
792 if resp.status == 415:
793 if parse_resp:
794 resp_body = self._parse_resp(resp_body)
795 raise exceptions.InvalidContentType(resp_body, resp=resp)
796
797 if resp.status == 422:
798 if parse_resp:
799 resp_body = self._parse_resp(resp_body)
800 raise exceptions.UnprocessableEntity(resp_body, resp=resp)
801
802 if resp.status in (500, 501):
803 message = resp_body
804 if parse_resp:
805 try:
806 resp_body = self._parse_resp(resp_body)
807 except ValueError:
808 # If response body is a non-json string message.
809 # Use resp_body as is and raise InvalidResponseBody
810 # exception.
811 raise exceptions.InvalidHTTPResponseBody(message)
812 else:
813 if isinstance(resp_body, dict):
814 # I'm seeing both computeFault
815 # and cloudServersFault come back.
816 # Will file a bug to fix, but leave as is for now.
817 if 'cloudServersFault' in resp_body:
818 message = resp_body['cloudServersFault']['message']
819 elif 'computeFault' in resp_body:
820 message = resp_body['computeFault']['message']
821 elif 'error' in resp_body:
822 message = resp_body['error']['message']
823 elif 'message' in resp_body:
824 message = resp_body['message']
825 else:
826 message = resp_body
827
828 if resp.status == 501:
829 raise exceptions.NotImplemented(resp_body, resp=resp,
830 message=message)
831 else:
832 raise exceptions.ServerFault(resp_body, resp=resp,
833 message=message)
834
835 if resp.status >= 400:
836 raise exceptions.UnexpectedResponseCode(str(resp.status),
837 resp=resp)
838
839 def is_absolute_limit(self, resp, resp_body):
840 if (not isinstance(resp_body, collections.Mapping) or
841 'retry-after' not in resp):
842 return True
Paul Glass119565a2016-04-06 11:41:42 -0500843 return 'exceed' in resp_body.get('message', 'blabla')
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500844
845 def wait_for_resource_deletion(self, id):
846 """Waits for a resource to be deleted
847
848 This method will loop over is_resource_deleted until either
849 is_resource_deleted returns True or the build timeout is reached. This
850 depends on is_resource_deleted being implemented
851
852 :param str id: The id of the resource to check
853 :raises TimeoutException: If the build_timeout has elapsed and the
854 resource still hasn't been deleted
855 """
856 start_time = int(time.time())
857 while True:
858 if self.is_resource_deleted(id):
859 return
860 if int(time.time()) - start_time >= self.build_timeout:
861 message = ('Failed to delete %(resource_type)s %(id)s within '
862 'the required time (%(timeout)s s).' %
863 {'resource_type': self.resource_type, 'id': id,
864 'timeout': self.build_timeout})
Jordan Pittier9e227c52016-02-09 14:35:18 +0100865 caller = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500866 if caller:
867 message = '(%s) %s' % (caller, message)
868 raise exceptions.TimeoutException(message)
869 time.sleep(self.build_interval)
870
871 def is_resource_deleted(self, id):
872 """Subclasses override with specific deletion detection."""
873 message = ('"%s" does not implement is_resource_deleted'
874 % self.__class__.__name__)
875 raise NotImplementedError(message)
876
877 @property
878 def resource_type(self):
879 """Returns the primary type of resource this client works with."""
880 return 'resource'
881
882 @classmethod
883 def validate_response(cls, schema, resp, body):
884 # Only check the response if the status code is a success code
885 # TODO(cyeoh): Eventually we should be able to verify that a failure
886 # code if it exists is something that we expect. This is explicitly
887 # declared in the V3 API and so we should be able to export this in
888 # the response schema. For now we'll ignore it.
889 if resp.status in HTTP_SUCCESS + HTTP_REDIRECTION:
890 cls.expected_success(schema['status_code'], resp.status)
891
892 # Check the body of a response
893 body_schema = schema.get('response_body')
894 if body_schema:
895 try:
896 jsonschema.validate(body, body_schema,
897 cls=JSONSCHEMA_VALIDATOR,
898 format_checker=FORMAT_CHECKER)
899 except jsonschema.ValidationError as ex:
guo yunxiana3f55282016-08-10 14:35:16 +0800900 msg = ("HTTP response body is invalid (%s)" % ex)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500901 raise exceptions.InvalidHTTPResponseBody(msg)
902 else:
903 if body:
guo yunxiana3f55282016-08-10 14:35:16 +0800904 msg = ("HTTP response body should not exist (%s)" % body)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500905 raise exceptions.InvalidHTTPResponseBody(msg)
906
907 # Check the header of a response
908 header_schema = schema.get('response_header')
909 if header_schema:
910 try:
911 jsonschema.validate(resp, header_schema,
912 cls=JSONSCHEMA_VALIDATOR,
913 format_checker=FORMAT_CHECKER)
914 except jsonschema.ValidationError as ex:
guo yunxiana3f55282016-08-10 14:35:16 +0800915 msg = ("HTTP response header is invalid (%s)" % ex)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500916 raise exceptions.InvalidHTTPResponseHeader(msg)
917
918
919class ResponseBody(dict):
920 """Class that wraps an http response and dict body into a single value.
921
922 Callers that receive this object will normally use it as a dict but
923 can extract the response if needed.
924 """
925
926 def __init__(self, response, body=None):
927 body_data = body or {}
928 self.update(body_data)
929 self.response = response
930
931 def __str__(self):
932 body = super(ResponseBody, self).__str__()
933 return "response: %s\nBody: %s" % (self.response, body)
934
935
936class ResponseBodyData(object):
937 """Class that wraps an http response and string data into a single value.
938
939 """
940
941 def __init__(self, response, data):
942 self.response = response
943 self.data = data
944
945 def __str__(self):
946 return "response: %s\nBody: %s" % (self.response, self.data)
947
948
949class ResponseBodyList(list):
950 """Class that wraps an http response and list body into a single value.
951
952 Callers that receive this object will normally use it as a list but
953 can extract the response if needed.
954 """
955
956 def __init__(self, response, body=None):
957 body_data = body or []
958 self.extend(body_data)
959 self.response = response
960
961 def __str__(self):
962 body = super(ResponseBodyList, self).__str__()
963 return "response: %s\nBody: %s" % (self.response, body)