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