blob: 5672bb78e8392ae3bc42ee22f005d2eac329c41a [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 logging as real_logging
20import re
21import time
22
23import jsonschema
24from oslo_log import log as logging
25from oslo_serialization import jsonutils as json
26import six
27
28from tempest.lib.common import http
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
42JSONSCHEMA_VALIDATOR = jsonschema.Draft4Validator
43FORMAT_CHECKER = jsonschema.draft4_format_checker
44
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
57 :param str endpoint_type: The endpoint type to use for the catalog lookup
58 :param int build_interval: Time in seconds between to status checks in
59 wait loops
60 :param int build_timeout: Timeout in seconds to wait for a wait operation.
61 :param bool disable_ssl_certificate_validation: Set to true to disable ssl
62 certificate validation
63 :param str ca_certs: File containing the CA Bundle to use in verifying a
64 TLS server cert
65 :param str trace_request: Regex to use for specifying logging the entirety
66 of the request and response payload
67 """
68 TYPE = "json"
69
70 # The version of the API this client implements
71 api_version = None
72
73 LOG = logging.getLogger(__name__)
74
75 def __init__(self, auth_provider, service, region,
76 endpoint_type='publicURL',
77 build_interval=1, build_timeout=60,
78 disable_ssl_certificate_validation=False, ca_certs=None,
79 trace_requests=''):
80 self.auth_provider = auth_provider
81 self.service = service
82 self.region = region
83 self.endpoint_type = endpoint_type
84 self.build_interval = build_interval
85 self.build_timeout = build_timeout
86 self.trace_requests = trace_requests
87
88 self._skip_path = False
89 self.general_header_lc = set(('cache-control', 'connection',
90 'date', 'pragma', 'trailer',
91 'transfer-encoding', 'via',
92 'warning'))
93 self.response_header_lc = set(('accept-ranges', 'age', 'etag',
94 'location', 'proxy-authenticate',
95 'retry-after', 'server',
96 'vary', 'www-authenticate'))
97 dscv = disable_ssl_certificate_validation
98 self.http_obj = http.ClosingHttp(
99 disable_ssl_certificate_validation=dscv, ca_certs=ca_certs)
100
101 def _get_type(self):
102 return self.TYPE
103
104 def get_headers(self, accept_type=None, send_type=None):
105 """Return the default headers which will be used with outgoing requests
106
107 :param str accept_type: The media type to use for the Accept header, if
108 one isn't provided the object var TYPE will be
109 used
110 :param str send_type: The media-type to use for the Content-Type
111 header, if one isn't provided the object var
112 TYPE will be used
113 :rtype: dict
114 :return: The dictionary of headers which can be used in the headers
115 dict for outgoing request
116 """
117 if accept_type is None:
118 accept_type = self._get_type()
119 if send_type is None:
120 send_type = self._get_type()
121 return {'Content-Type': 'application/%s' % send_type,
122 'Accept': 'application/%s' % accept_type}
123
124 def __str__(self):
125 STRING_LIMIT = 80
126 str_format = ("service:%s, base_url:%s, "
127 "filters: %s, build_interval:%s, build_timeout:%s"
128 "\ntoken:%s..., \nheaders:%s...")
129 return str_format % (self.service, self.base_url,
130 self.filters, self.build_interval,
131 self.build_timeout,
132 str(self.token)[0:STRING_LIMIT],
133 str(self.get_headers())[0:STRING_LIMIT])
134
135 @property
136 def user(self):
137 """The username used for requests
138
139 :rtype: string
140 :return: The username being used for requests
141 """
142
143 return self.auth_provider.credentials.username
144
145 @property
146 def user_id(self):
147 """The user_id used for requests
148
149 :rtype: string
150 :return: The user id being used for requests
151 """
152 return self.auth_provider.credentials.user_id
153
154 @property
155 def tenant_name(self):
156 """The tenant/project being used for requests
157
158 :rtype: string
159 :return: The tenant/project name being used for requests
160 """
161 return self.auth_provider.credentials.tenant_name
162
163 @property
164 def tenant_id(self):
165 """The tenant/project id being used for requests
166
167 :rtype: string
168 :return: The tenant/project id being used for requests
169 """
170 return self.auth_provider.credentials.tenant_id
171
172 @property
173 def password(self):
174 """The password being used for requests
175
176 :rtype: string
177 :return: The password being used for requests
178 """
179 return self.auth_provider.credentials.password
180
181 @property
182 def base_url(self):
183 return self.auth_provider.base_url(filters=self.filters)
184
185 @property
186 def token(self):
187 return self.auth_provider.get_token()
188
189 @property
190 def filters(self):
191 _filters = dict(
192 service=self.service,
193 endpoint_type=self.endpoint_type,
194 region=self.region
195 )
196 if self.api_version is not None:
197 _filters['api_version'] = self.api_version
198 if self._skip_path:
199 _filters['skip_path'] = self._skip_path
200 return _filters
201
202 def skip_path(self):
203 """When set, ignore the path part of the base URL from the catalog"""
204 self._skip_path = True
205
206 def reset_path(self):
207 """When reset, use the base URL from the catalog as-is"""
208 self._skip_path = False
209
210 @classmethod
211 def expected_success(cls, expected_code, read_code):
212 """Check expected success response code against the http response
213
214 :param int expected_code: The response code that is expected.
215 Optionally a list of integers can be used
216 to specify multiple valid success codes
217 :param int read_code: The response code which was returned in the
218 response
219 :raises AssertionError: if the expected_code isn't a valid http success
220 response code
221 :raises exceptions.InvalidHttpSuccessCode: if the read code isn't an
222 expected http success code
223 """
224 assert_msg = ("This function only allowed to use for HTTP status"
225 "codes which explicitly defined in the RFC 7231 & 4918."
226 "{0} is not a defined Success Code!"
227 ).format(expected_code)
228 if isinstance(expected_code, list):
229 for code in expected_code:
230 assert code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg
231 else:
232 assert expected_code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg
233
234 # NOTE(afazekas): the http status code above 400 is processed by
235 # the _error_checker method
236 if read_code < 400:
237 pattern = """Unexpected http success status code {0},
238 The expected status code is {1}"""
239 if ((not isinstance(expected_code, list) and
240 (read_code != expected_code)) or
241 (isinstance(expected_code, list) and
242 (read_code not in expected_code))):
243 details = pattern.format(read_code, expected_code)
244 raise exceptions.InvalidHttpSuccessCode(details)
245
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200246 def post(self, url, body, headers=None, extra_headers=False,
247 chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500248 """Send a HTTP POST request using keystone auth
249
250 :param str url: the relative url to send the post request to
251 :param dict body: the request body
252 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300253 :param bool extra_headers: Boolean value than indicates if the headers
254 returned by the get_headers() method are to
255 be used but additional headers are needed in
256 the request pass them in as a dict.
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200257 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500258 :return: a tuple with the first entry containing the response headers
259 and the second the response body
260 :rtype: tuple
261 """
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200262 return self.request('POST', url, extra_headers, headers, body, chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500263
264 def get(self, url, headers=None, extra_headers=False):
265 """Send a HTTP GET request using keystone service catalog and auth
266
267 :param str url: the relative url to send the post request to
268 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300269 :param bool extra_headers: Boolean value than indicates if the headers
270 returned by the get_headers() method are to
271 be used but additional headers are needed in
272 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500273 :return: a tuple with the first entry containing the response headers
274 and the second the response body
275 :rtype: tuple
276 """
277 return self.request('GET', url, extra_headers, headers)
278
279 def delete(self, url, headers=None, body=None, extra_headers=False):
280 """Send a HTTP DELETE request using keystone service catalog and auth
281
282 :param str url: the relative url to send the post request to
283 :param dict headers: The headers to use for the request
284 :param dict body: the request body
vsaienko9eb846b2016-04-09 00:35:47 +0300285 :param bool extra_headers: Boolean value than indicates if the headers
286 returned by the get_headers() method are to
287 be used but additional headers are needed in
288 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500289 :return: a tuple with the first entry containing the response headers
290 and the second the response body
291 :rtype: tuple
292 """
293 return self.request('DELETE', url, extra_headers, headers, body)
294
295 def patch(self, url, body, headers=None, extra_headers=False):
296 """Send a HTTP PATCH request using keystone service catalog and auth
297
298 :param str url: the relative url to send the post request to
299 :param dict body: the request body
300 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300301 :param bool extra_headers: Boolean value than indicates if the headers
302 returned by the get_headers() method are to
303 be used but additional headers are needed in
304 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500305 :return: a tuple with the first entry containing the response headers
306 and the second the response body
307 :rtype: tuple
308 """
309 return self.request('PATCH', url, extra_headers, headers, body)
310
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200311 def put(self, url, body, headers=None, extra_headers=False, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500312 """Send a HTTP PUT request using keystone service catalog and auth
313
314 :param str url: the relative url to send the post request to
315 :param dict body: the request body
316 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300317 :param bool extra_headers: Boolean value than indicates if the headers
318 returned by the get_headers() method are to
319 be used but additional headers are needed in
320 the request pass them in as a dict.
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200321 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500322 :return: a tuple with the first entry containing the response headers
323 and the second the response body
324 :rtype: tuple
325 """
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200326 return self.request('PUT', url, extra_headers, headers, body, chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500327
328 def head(self, url, headers=None, extra_headers=False):
329 """Send a HTTP HEAD request using keystone service catalog and auth
330
331 :param str url: the relative url to send the post request to
332 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300333 :param bool extra_headers: Boolean value than indicates if the headers
334 returned by the get_headers() method are to
335 be used but additional headers are needed in
336 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500337 :return: a tuple with the first entry containing the response headers
338 and the second the response body
339 :rtype: tuple
340 """
341 return self.request('HEAD', url, extra_headers, headers)
342
343 def copy(self, url, headers=None, extra_headers=False):
344 """Send a HTTP COPY request using keystone service catalog and auth
345
346 :param str url: the relative url to send the post request to
347 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300348 :param bool extra_headers: Boolean value than indicates if the headers
349 returned by the get_headers() method are to
350 be used but additional headers are needed in
351 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500352 :return: a tuple with the first entry containing the response headers
353 and the second the response body
354 :rtype: tuple
355 """
356 return self.request('COPY', url, extra_headers, headers)
357
358 def get_versions(self):
359 """Get the versions on a endpoint from the keystone catalog
360
361 This method will make a GET request on the baseurl from the keystone
362 catalog to return a list of API versions. It is expected that a GET
363 on the endpoint in the catalog will return a list of supported API
364 versions.
365
366 :return tuple with response headers and list of version numbers
367 :rtype: tuple
368 """
369 resp, body = self.get('')
370 body = self._parse_resp(body)
371 versions = map(lambda x: x['id'], body)
372 return resp, versions
373
374 def _get_request_id(self, resp):
375 for i in ('x-openstack-request-id', 'x-compute-request-id'):
376 if i in resp:
377 return resp[i]
378 return ""
379
380 def _safe_body(self, body, maxlen=4096):
381 # convert a structure into a string safely
382 try:
383 text = six.text_type(body)
384 except UnicodeDecodeError:
385 # if this isn't actually text, return marker that
386 return "<BinaryData: removed>"
387 if len(text) > maxlen:
388 return text[:maxlen]
389 else:
390 return text
391
392 def _log_request_start(self, method, req_url, req_headers=None,
393 req_body=None):
394 if req_headers is None:
395 req_headers = {}
Jordan Pittier9e227c52016-02-09 14:35:18 +0100396 caller_name = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500397 if self.trace_requests and re.search(self.trace_requests, caller_name):
398 self.LOG.debug('Starting Request (%s): %s %s' %
399 (caller_name, method, req_url))
400
401 def _log_request_full(self, method, req_url, resp,
402 secs="", req_headers=None,
403 req_body=None, resp_body=None,
404 caller_name=None, extra=None):
405 if 'X-Auth-Token' in req_headers:
406 req_headers['X-Auth-Token'] = '<omitted>'
Andrea Frittoli (andreaf)a1edb2d2016-05-10 16:09:59 +0100407 # A shallow copy is sufficient
408 resp_log = resp.copy()
409 if 'x-subject-token' in resp_log:
410 resp_log['x-subject-token'] = '<omitted>'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500411 log_fmt = """Request - Headers: %s
412 Body: %s
413 Response - Headers: %s
414 Body: %s"""
415
416 self.LOG.debug(
417 log_fmt % (
418 str(req_headers),
419 self._safe_body(req_body),
Andrea Frittoli (andreaf)a1edb2d2016-05-10 16:09:59 +0100420 str(resp_log),
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500421 self._safe_body(resp_body)),
422 extra=extra)
423
424 def _log_request(self, method, req_url, resp,
425 secs="", req_headers=None,
426 req_body=None, resp_body=None):
427 if req_headers is None:
428 req_headers = {}
429 # if we have the request id, put it in the right part of the log
430 extra = dict(request_id=self._get_request_id(resp))
431 # NOTE(sdague): while we still have 6 callers to this function
432 # we're going to just provide work around on who is actually
433 # providing timings by gracefully adding no content if they don't.
434 # Once we're down to 1 caller, clean this up.
Jordan Pittier9e227c52016-02-09 14:35:18 +0100435 caller_name = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500436 if secs:
437 secs = " %.3fs" % secs
438 self.LOG.info(
439 'Request (%s): %s %s %s%s' % (
440 caller_name,
441 resp['status'],
442 method,
443 req_url,
444 secs),
445 extra=extra)
446
447 # Also look everything at DEBUG if you want to filter this
448 # out, don't run at debug.
449 if self.LOG.isEnabledFor(real_logging.DEBUG):
450 self._log_request_full(method, req_url, resp, secs, req_headers,
451 req_body, resp_body, caller_name, extra)
452
453 def _parse_resp(self, body):
454 try:
455 body = json.loads(body)
456 except ValueError:
457 return body
458
459 # We assume, that if the first value of the deserialized body's
460 # item set is a dict or a list, that we just return the first value
461 # of deserialized body.
462 # Essentially "cutting out" the first placeholder element in a body
463 # that looks like this:
464 #
465 # {
466 # "users": [
467 # ...
468 # ]
469 # }
470 try:
471 # Ensure there are not more than one top-level keys
472 # NOTE(freerunner): Ensure, that JSON is not nullable to
473 # to prevent StopIteration Exception
474 if len(body.keys()) != 1:
475 return body
476 # Just return the "wrapped" element
477 first_key, first_item = six.next(six.iteritems(body))
478 if isinstance(first_item, (dict, list)):
479 return first_item
480 except (ValueError, IndexError):
481 pass
482 return body
483
484 def response_checker(self, method, resp, resp_body):
485 """A sanity check on the response from a HTTP request
486
487 This method does a sanity check on whether the response from an HTTP
488 request conforms the HTTP RFC.
489
490 :param str method: The HTTP verb of the request associated with the
491 response being passed in.
492 :param resp: The response headers
493 :param resp_body: The body of the response
494 :raises ResponseWithNonEmptyBody: If the response with the status code
495 is not supposed to have a body
496 :raises ResponseWithEntity: If the response code is 205 but has an
497 entity
498 """
499 if (resp.status in set((204, 205, 304)) or resp.status < 200 or
500 method.upper() == 'HEAD') and resp_body:
501 raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
502 # NOTE(afazekas):
503 # If the HTTP Status Code is 205
504 # 'The response MUST NOT include an entity.'
505 # A HTTP entity has an entity-body and an 'entity-header'.
506 # In the HTTP response specification (Section 6) the 'entity-header'
507 # 'generic-header' and 'response-header' are in OR relation.
508 # All headers not in the above two group are considered as entity
509 # header in every interpretation.
510
511 if (resp.status == 205 and
512 0 != len(set(resp.keys()) - set(('status',)) -
513 self.response_header_lc - self.general_header_lc)):
514 raise exceptions.ResponseWithEntity()
515 # NOTE(afazekas)
516 # Now the swift sometimes (delete not empty container)
517 # returns with non json error response, we can create new rest class
518 # for swift.
519 # Usually RFC2616 says error responses SHOULD contain an explanation.
520 # The warning is normal for SHOULD/SHOULD NOT case
521
522 # Likely it will cause an error
523 if method != 'HEAD' and not resp_body and resp.status >= 400:
524 self.LOG.warning("status >= 400 response with empty body")
525
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200526 def _request(self, method, url, headers=None, body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500527 """A simple HTTP request interface."""
528 # Authenticate the request with the auth provider
529 req_url, req_headers, req_body = self.auth_provider.auth_request(
530 method, url, headers, body, self.filters)
531
532 # Do the actual request, and time it
533 start = time.time()
534 self._log_request_start(method, req_url)
535 resp, resp_body = self.raw_request(
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200536 req_url, method, headers=req_headers, body=req_body,
537 chunked=chunked
538 )
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500539 end = time.time()
540 self._log_request(method, req_url, resp, secs=(end - start),
541 req_headers=req_headers, req_body=req_body,
542 resp_body=resp_body)
543
544 # Verify HTTP response codes
545 self.response_checker(method, resp, resp_body)
546
547 return resp, resp_body
548
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200549 def raw_request(self, url, method, headers=None, body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500550 """Send a raw HTTP request without the keystone catalog or auth
551
552 This method sends a HTTP request in the same manner as the request()
553 method, however it does so without using keystone auth or the catalog
554 to determine the base url. Additionally no response handling is done
555 the results from the request are just returned.
556
557 :param str url: Full url to send the request
558 :param str method: The HTTP verb to use for the request
559 :param str headers: Headers to use for the request if none are specifed
560 the headers
Anh Trand44a8be2016-03-25 09:49:14 +0700561 :param str body: Body to send with the request
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200562 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500563 :rtype: tuple
564 :return: a tuple with the first entry containing the response headers
565 and the second the response body
566 """
567 if headers is None:
568 headers = self.get_headers()
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200569 return self.http_obj.request(url, method, headers=headers,
570 body=body, chunked=chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500571
572 def request(self, method, url, extra_headers=False, headers=None,
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200573 body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500574 """Send a HTTP request with keystone auth and using the catalog
575
576 This method will send an HTTP request using keystone auth in the
577 headers and the catalog to determine the endpoint to use for the
578 baseurl to send the request to. Additionally
579
580 When a response is received it will check it to see if an error
581 response was received. If it was an exception will be raised to enable
582 it to be handled quickly.
583
584 This method will also handle rate-limiting, if a 413 response code is
585 received it will retry the request after waiting the 'retry-after'
586 duration from the header.
587
588 :param str method: The HTTP verb to use for the request
589 :param str url: Relative url to send the request to
vsaienko9eb846b2016-04-09 00:35:47 +0300590 :param bool extra_headers: Boolean value than indicates if the headers
591 returned by the get_headers() method are to
592 be used but additional headers are needed in
593 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500594 :param dict headers: Headers to use for the request if none are
595 specifed the headers returned from the
596 get_headers() method are used. If the request
597 explicitly requires no headers use an empty dict.
Anh Trand44a8be2016-03-25 09:49:14 +0700598 :param str body: Body to send with the request
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200599 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500600 :rtype: tuple
601 :return: a tuple with the first entry containing the response headers
602 and the second the response body
603 :raises UnexpectedContentType: If the content-type of the response
604 isn't an expect type
605 :raises Unauthorized: If a 401 response code is received
606 :raises Forbidden: If a 403 response code is received
607 :raises NotFound: If a 404 response code is received
608 :raises BadRequest: If a 400 response code is received
609 :raises Gone: If a 410 response code is received
610 :raises Conflict: If a 409 response code is received
611 :raises OverLimit: If a 413 response code is received and over_limit is
612 not in the response body
613 :raises RateLimitExceeded: If a 413 response code is received and
614 over_limit is in the response body
615 :raises InvalidContentType: If a 415 response code is received
616 :raises UnprocessableEntity: If a 422 response code is received
617 :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
618 and couldn't be parsed
619 :raises NotImplemented: If a 501 response code is received
620 :raises ServerFault: If a 500 response code is received
621 :raises UnexpectedResponseCode: If a response code above 400 is
622 received and it doesn't fall into any
623 of the handled checks
624 """
625 # if extra_headers is True
626 # default headers would be added to headers
627 retry = 0
628
629 if headers is None:
630 # NOTE(vponomaryov): if some client do not need headers,
631 # it should explicitly pass empty dict
632 headers = self.get_headers()
633 elif extra_headers:
634 try:
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500635 headers.update(self.get_headers())
636 except (ValueError, TypeError):
637 headers = self.get_headers()
638
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200639 resp, resp_body = self._request(method, url, headers=headers,
640 body=body, chunked=chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500641
642 while (resp.status == 413 and
643 'retry-after' in resp and
644 not self.is_absolute_limit(
645 resp, self._parse_resp(resp_body)) and
646 retry < MAX_RECURSION_DEPTH):
647 retry += 1
Paul Glass119565a2016-04-06 11:41:42 -0500648 delay = self._get_retry_after_delay(resp)
649 self.LOG.debug(
650 "Sleeping %s seconds based on retry-after header", delay
651 )
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500652 time.sleep(delay)
653 resp, resp_body = self._request(method, url,
654 headers=headers, body=body)
655 self._error_checker(method, url, headers, body,
656 resp, resp_body)
657 return resp, resp_body
658
Paul Glass119565a2016-04-06 11:41:42 -0500659 def _get_retry_after_delay(self, resp):
660 """Extract the delay from the retry-after header.
661
662 This supports both integer and HTTP date formatted retry-after headers
663 per RFC 2616.
664
665 :param resp: The response containing the retry-after headers
666 :rtype: int
667 :return: The delay in seconds, clamped to be at least 1 second
668 :raises ValueError: On failing to parse the delay
669 """
670 delay = None
671 try:
672 delay = int(resp['retry-after'])
673 except (ValueError, KeyError):
674 pass
675
676 try:
677 retry_timestamp = self._parse_http_date(resp['retry-after'])
678 date_timestamp = self._parse_http_date(resp['date'])
679 delay = int(retry_timestamp - date_timestamp)
680 except (ValueError, OverflowError, KeyError):
681 pass
682
683 if delay is None:
684 raise ValueError(
685 "Failed to parse retry-after header %r as either int or "
686 "HTTP-date." % resp.get('retry-after')
687 )
688
689 # Retry-after headers do not have sub-second precision. Clients may
690 # receive a delay of 0. After sleeping 0 seconds, we would (likely) hit
691 # another 413. To avoid this, always sleep at least 1 second.
692 return max(1, delay)
693
694 def _parse_http_date(self, val):
695 """Parse an HTTP date, like 'Fri, 31 Dec 1999 23:59:59 GMT'.
696
697 Return an epoch timestamp (float), as returned by time.mktime().
698 """
699 parts = email.utils.parsedate(val)
700 if not parts:
701 raise ValueError("Failed to parse date %s" % val)
702 return time.mktime(parts)
703
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500704 def _error_checker(self, method, url,
705 headers, body, resp, resp_body):
706
707 # NOTE(mtreinish): Check for httplib response from glance_http. The
708 # object can't be used here because importing httplib breaks httplib2.
709 # If another object from a class not imported were passed here as
710 # resp this could possibly fail
711 if str(type(resp)) == "<type 'instance'>":
712 ctype = resp.getheader('content-type')
713 else:
714 try:
715 ctype = resp['content-type']
716 # NOTE(mtreinish): Keystone delete user responses doesn't have a
717 # content-type header. (They don't have a body) So just pretend it
718 # is set.
719 except KeyError:
720 ctype = 'application/json'
721
722 # It is not an error response
723 if resp.status < 400:
724 return
725
726 JSON_ENC = ['application/json', 'application/json; charset=utf-8']
727 # NOTE(mtreinish): This is for compatibility with Glance and swift
728 # APIs. These are the return content types that Glance api v1
729 # (and occasionally swift) are using.
730 TXT_ENC = ['text/plain', 'text/html', 'text/html; charset=utf-8',
731 'text/plain; charset=utf-8']
732
733 if ctype.lower() in JSON_ENC:
734 parse_resp = True
735 elif ctype.lower() in TXT_ENC:
736 parse_resp = False
737 else:
738 raise exceptions.UnexpectedContentType(str(resp.status),
739 resp=resp)
740
741 if resp.status == 401:
742 if parse_resp:
743 resp_body = self._parse_resp(resp_body)
744 raise exceptions.Unauthorized(resp_body, resp=resp)
745
746 if resp.status == 403:
747 if parse_resp:
748 resp_body = self._parse_resp(resp_body)
749 raise exceptions.Forbidden(resp_body, resp=resp)
750
751 if resp.status == 404:
752 if parse_resp:
753 resp_body = self._parse_resp(resp_body)
754 raise exceptions.NotFound(resp_body, resp=resp)
755
756 if resp.status == 400:
757 if parse_resp:
758 resp_body = self._parse_resp(resp_body)
759 raise exceptions.BadRequest(resp_body, resp=resp)
760
761 if resp.status == 410:
762 if parse_resp:
763 resp_body = self._parse_resp(resp_body)
764 raise exceptions.Gone(resp_body, resp=resp)
765
766 if resp.status == 409:
767 if parse_resp:
768 resp_body = self._parse_resp(resp_body)
769 raise exceptions.Conflict(resp_body, resp=resp)
770
771 if resp.status == 413:
772 if parse_resp:
773 resp_body = self._parse_resp(resp_body)
774 if self.is_absolute_limit(resp, resp_body):
775 raise exceptions.OverLimit(resp_body, resp=resp)
776 else:
777 raise exceptions.RateLimitExceeded(resp_body, resp=resp)
778
779 if resp.status == 415:
780 if parse_resp:
781 resp_body = self._parse_resp(resp_body)
782 raise exceptions.InvalidContentType(resp_body, resp=resp)
783
784 if resp.status == 422:
785 if parse_resp:
786 resp_body = self._parse_resp(resp_body)
787 raise exceptions.UnprocessableEntity(resp_body, resp=resp)
788
789 if resp.status in (500, 501):
790 message = resp_body
791 if parse_resp:
792 try:
793 resp_body = self._parse_resp(resp_body)
794 except ValueError:
795 # If response body is a non-json string message.
796 # Use resp_body as is and raise InvalidResponseBody
797 # exception.
798 raise exceptions.InvalidHTTPResponseBody(message)
799 else:
800 if isinstance(resp_body, dict):
801 # I'm seeing both computeFault
802 # and cloudServersFault come back.
803 # Will file a bug to fix, but leave as is for now.
804 if 'cloudServersFault' in resp_body:
805 message = resp_body['cloudServersFault']['message']
806 elif 'computeFault' in resp_body:
807 message = resp_body['computeFault']['message']
808 elif 'error' in resp_body:
809 message = resp_body['error']['message']
810 elif 'message' in resp_body:
811 message = resp_body['message']
812 else:
813 message = resp_body
814
815 if resp.status == 501:
816 raise exceptions.NotImplemented(resp_body, resp=resp,
817 message=message)
818 else:
819 raise exceptions.ServerFault(resp_body, resp=resp,
820 message=message)
821
822 if resp.status >= 400:
823 raise exceptions.UnexpectedResponseCode(str(resp.status),
824 resp=resp)
825
826 def is_absolute_limit(self, resp, resp_body):
827 if (not isinstance(resp_body, collections.Mapping) or
828 'retry-after' not in resp):
829 return True
Paul Glass119565a2016-04-06 11:41:42 -0500830 return 'exceed' in resp_body.get('message', 'blabla')
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500831
832 def wait_for_resource_deletion(self, id):
833 """Waits for a resource to be deleted
834
835 This method will loop over is_resource_deleted until either
836 is_resource_deleted returns True or the build timeout is reached. This
837 depends on is_resource_deleted being implemented
838
839 :param str id: The id of the resource to check
840 :raises TimeoutException: If the build_timeout has elapsed and the
841 resource still hasn't been deleted
842 """
843 start_time = int(time.time())
844 while True:
845 if self.is_resource_deleted(id):
846 return
847 if int(time.time()) - start_time >= self.build_timeout:
848 message = ('Failed to delete %(resource_type)s %(id)s within '
849 'the required time (%(timeout)s s).' %
850 {'resource_type': self.resource_type, 'id': id,
851 'timeout': self.build_timeout})
Jordan Pittier9e227c52016-02-09 14:35:18 +0100852 caller = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500853 if caller:
854 message = '(%s) %s' % (caller, message)
855 raise exceptions.TimeoutException(message)
856 time.sleep(self.build_interval)
857
858 def is_resource_deleted(self, id):
859 """Subclasses override with specific deletion detection."""
860 message = ('"%s" does not implement is_resource_deleted'
861 % self.__class__.__name__)
862 raise NotImplementedError(message)
863
864 @property
865 def resource_type(self):
866 """Returns the primary type of resource this client works with."""
867 return 'resource'
868
869 @classmethod
870 def validate_response(cls, schema, resp, body):
871 # Only check the response if the status code is a success code
872 # TODO(cyeoh): Eventually we should be able to verify that a failure
873 # code if it exists is something that we expect. This is explicitly
874 # declared in the V3 API and so we should be able to export this in
875 # the response schema. For now we'll ignore it.
876 if resp.status in HTTP_SUCCESS + HTTP_REDIRECTION:
877 cls.expected_success(schema['status_code'], resp.status)
878
879 # Check the body of a response
880 body_schema = schema.get('response_body')
881 if body_schema:
882 try:
883 jsonschema.validate(body, body_schema,
884 cls=JSONSCHEMA_VALIDATOR,
885 format_checker=FORMAT_CHECKER)
886 except jsonschema.ValidationError as ex:
887 msg = ("HTTP response body is invalid (%s)") % ex
888 raise exceptions.InvalidHTTPResponseBody(msg)
889 else:
890 if body:
891 msg = ("HTTP response body should not exist (%s)") % body
892 raise exceptions.InvalidHTTPResponseBody(msg)
893
894 # Check the header of a response
895 header_schema = schema.get('response_header')
896 if header_schema:
897 try:
898 jsonschema.validate(resp, header_schema,
899 cls=JSONSCHEMA_VALIDATOR,
900 format_checker=FORMAT_CHECKER)
901 except jsonschema.ValidationError as ex:
902 msg = ("HTTP response header is invalid (%s)") % ex
903 raise exceptions.InvalidHTTPResponseHeader(msg)
904
905
906class ResponseBody(dict):
907 """Class that wraps an http response and dict body into a single value.
908
909 Callers that receive this object will normally use it as a dict but
910 can extract the response if needed.
911 """
912
913 def __init__(self, response, body=None):
914 body_data = body or {}
915 self.update(body_data)
916 self.response = response
917
918 def __str__(self):
919 body = super(ResponseBody, self).__str__()
920 return "response: %s\nBody: %s" % (self.response, body)
921
922
923class ResponseBodyData(object):
924 """Class that wraps an http response and string data into a single value.
925
926 """
927
928 def __init__(self, response, data):
929 self.response = response
930 self.data = data
931
932 def __str__(self):
933 return "response: %s\nBody: %s" % (self.response, self.data)
934
935
936class ResponseBodyList(list):
937 """Class that wraps an http response and list body into a single value.
938
939 Callers that receive this object will normally use it as a list but
940 can extract the response if needed.
941 """
942
943 def __init__(self, response, body=None):
944 body_data = body or []
945 self.extend(body_data)
946 self.response = response
947
948 def __str__(self):
949 body = super(ResponseBodyList, self).__str__()
950 return "response: %s\nBody: %s" % (self.response, body)