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