blob: e2fd7220ede91fc90aa63dc0d91e2d47057e4e43 [file] [log] [blame]
Matthew Treinish9e26ca82016-02-23 11:43:20 -05001# Copyright 2012 OpenStack Foundation
2# Copyright 2013 IBM Corp.
3# All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
17import collections
Paul Glass119565a2016-04-06 11:41:42 -050018import email.utils
Matthew Treinish9e26ca82016-02-23 11:43:20 -050019import re
20import time
21
22import jsonschema
23from oslo_log import log as logging
24from oslo_serialization import jsonutils as json
25import six
Ken'ichi Ohmichida26b162017-03-03 15:53:46 -080026from six.moves import urllib
Matthew Treinish9e26ca82016-02-23 11:43:20 -050027
28from tempest.lib.common import http
ghanshyamf9ded352016-04-12 17:03:01 +090029from tempest.lib.common import jsonschema_validator
Jordan Pittier9e227c52016-02-09 14:35:18 +010030from tempest.lib.common.utils import test_utils
Matthew Treinish9e26ca82016-02-23 11:43:20 -050031from tempest.lib import exceptions
32
33# redrive rate limited calls at most twice
34MAX_RECURSION_DEPTH = 2
35
36# All the successful HTTP status codes from RFC 7231 & 4918
37HTTP_SUCCESS = (200, 201, 202, 203, 204, 205, 206, 207)
38
39# All the redirection HTTP status codes from RFC 7231 & 4918
40HTTP_REDIRECTION = (300, 301, 302, 303, 304, 305, 306, 307)
41
42# JSON Schema validator and format checker used for JSON Schema validation
ghanshyamf9ded352016-04-12 17:03:01 +090043JSONSCHEMA_VALIDATOR = jsonschema_validator.JSONSCHEMA_VALIDATOR
44FORMAT_CHECKER = jsonschema_validator.FORMAT_CHECKER
Matthew Treinish9e26ca82016-02-23 11:43:20 -050045
46
47class RestClient(object):
48 """Unified OpenStack RestClient class
49
50 This class is used for building openstack api clients on top of. It is
51 intended to provide a base layer for wrapping outgoing http requests in
52 keystone auth as well as providing response code checking and error
53 handling.
54
55 :param auth_provider: an auth provider object used to wrap requests in auth
56 :param str service: The service name to use for the catalog lookup
57 :param str region: The region to use for the catalog lookup
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -060058 :param str name: The endpoint name to use for the catalog lookup; this
59 returns only if the service exists
Matthew Treinish9e26ca82016-02-23 11:43:20 -050060 :param str endpoint_type: The endpoint type to use for the catalog lookup
61 :param int build_interval: Time in seconds between to status checks in
62 wait loops
63 :param int build_timeout: Timeout in seconds to wait for a wait operation.
64 :param bool disable_ssl_certificate_validation: Set to true to disable ssl
65 certificate validation
66 :param str ca_certs: File containing the CA Bundle to use in verifying a
67 TLS server cert
guo yunxian6f24cc42016-07-29 20:03:41 +080068 :param str trace_requests: Regex to use for specifying logging the entirety
Matthew Treinish9e26ca82016-02-23 11:43:20 -050069 of the request and response payload
zhufl071e94c2016-07-12 10:26:34 +080070 :param str http_timeout: Timeout in seconds to wait for the http request to
71 return
Matthew Treinish74514402016-09-01 11:44:57 -040072 :param str proxy_url: http proxy url to use.
Matthew Treinish9e26ca82016-02-23 11:43:20 -050073 """
Matthew Treinish9e26ca82016-02-23 11:43:20 -050074
75 # The version of the API this client implements
76 api_version = None
77
78 LOG = logging.getLogger(__name__)
79
80 def __init__(self, auth_provider, service, region,
81 endpoint_type='publicURL',
82 build_interval=1, build_timeout=60,
83 disable_ssl_certificate_validation=False, ca_certs=None,
Matthew Treinish74514402016-09-01 11:44:57 -040084 trace_requests='', name=None, http_timeout=None,
85 proxy_url=None):
Matthew Treinish9e26ca82016-02-23 11:43:20 -050086 self.auth_provider = auth_provider
87 self.service = service
88 self.region = region
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -060089 self.name = name
Matthew Treinish9e26ca82016-02-23 11:43:20 -050090 self.endpoint_type = endpoint_type
91 self.build_interval = build_interval
92 self.build_timeout = build_timeout
93 self.trace_requests = trace_requests
94
95 self._skip_path = False
96 self.general_header_lc = set(('cache-control', 'connection',
97 'date', 'pragma', 'trailer',
98 'transfer-encoding', 'via',
99 'warning'))
100 self.response_header_lc = set(('accept-ranges', 'age', 'etag',
101 'location', 'proxy-authenticate',
102 'retry-after', 'server',
103 'vary', 'www-authenticate'))
104 dscv = disable_ssl_certificate_validation
Matthew Treinish74514402016-09-01 11:44:57 -0400105
106 if proxy_url:
107 self.http_obj = http.ClosingProxyHttp(
108 proxy_url,
109 disable_ssl_certificate_validation=dscv, ca_certs=ca_certs,
110 timeout=http_timeout)
111 else:
112 self.http_obj = http.ClosingHttp(
113 disable_ssl_certificate_validation=dscv, ca_certs=ca_certs,
114 timeout=http_timeout)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500115
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500116 def get_headers(self, accept_type=None, send_type=None):
117 """Return the default headers which will be used with outgoing requests
118
119 :param str accept_type: The media type to use for the Accept header, if
120 one isn't provided the object var TYPE will be
121 used
122 :param str send_type: The media-type to use for the Content-Type
123 header, if one isn't provided the object var
124 TYPE will be used
125 :rtype: dict
126 :return: The dictionary of headers which can be used in the headers
127 dict for outgoing request
128 """
129 if accept_type is None:
Masayuki Igawa189b92f2017-04-24 18:57:17 +0900130 accept_type = 'json'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500131 if send_type is None:
Masayuki Igawa189b92f2017-04-24 18:57:17 +0900132 send_type = 'json'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500133 return {'Content-Type': 'application/%s' % send_type,
134 'Accept': 'application/%s' % accept_type}
135
136 def __str__(self):
137 STRING_LIMIT = 80
138 str_format = ("service:%s, base_url:%s, "
139 "filters: %s, build_interval:%s, build_timeout:%s"
140 "\ntoken:%s..., \nheaders:%s...")
141 return str_format % (self.service, self.base_url,
142 self.filters, self.build_interval,
143 self.build_timeout,
144 str(self.token)[0:STRING_LIMIT],
145 str(self.get_headers())[0:STRING_LIMIT])
146
147 @property
148 def user(self):
149 """The username used for requests
150
151 :rtype: string
152 :return: The username being used for requests
153 """
154
155 return self.auth_provider.credentials.username
156
157 @property
158 def user_id(self):
159 """The user_id used for requests
160
161 :rtype: string
162 :return: The user id being used for requests
163 """
164 return self.auth_provider.credentials.user_id
165
166 @property
167 def tenant_name(self):
168 """The tenant/project being used for requests
169
170 :rtype: string
171 :return: The tenant/project name being used for requests
172 """
173 return self.auth_provider.credentials.tenant_name
174
175 @property
176 def tenant_id(self):
177 """The tenant/project id being used for requests
178
179 :rtype: string
180 :return: The tenant/project id being used for requests
181 """
182 return self.auth_provider.credentials.tenant_id
183
184 @property
185 def password(self):
186 """The password being used for requests
187
188 :rtype: string
189 :return: The password being used for requests
190 """
191 return self.auth_provider.credentials.password
192
193 @property
194 def base_url(self):
195 return self.auth_provider.base_url(filters=self.filters)
196
197 @property
198 def token(self):
199 return self.auth_provider.get_token()
200
201 @property
202 def filters(self):
203 _filters = dict(
204 service=self.service,
205 endpoint_type=self.endpoint_type,
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -0600206 region=self.region,
207 name=self.name
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500208 )
209 if self.api_version is not None:
210 _filters['api_version'] = self.api_version
211 if self._skip_path:
212 _filters['skip_path'] = self._skip_path
213 return _filters
214
215 def skip_path(self):
216 """When set, ignore the path part of the base URL from the catalog"""
217 self._skip_path = True
218
219 def reset_path(self):
220 """When reset, use the base URL from the catalog as-is"""
221 self._skip_path = False
222
223 @classmethod
224 def expected_success(cls, expected_code, read_code):
225 """Check expected success response code against the http response
226
227 :param int expected_code: The response code that is expected.
228 Optionally a list of integers can be used
229 to specify multiple valid success codes
230 :param int read_code: The response code which was returned in the
231 response
232 :raises AssertionError: if the expected_code isn't a valid http success
233 response code
234 :raises exceptions.InvalidHttpSuccessCode: if the read code isn't an
235 expected http success code
236 """
ghanshyamc3074202016-04-18 15:20:45 +0900237 if not isinstance(read_code, int):
238 raise TypeError("'read_code' must be an int instead of (%s)"
239 % type(read_code))
240
Hanxi2f977db2016-09-01 17:31:28 +0800241 assert_msg = ("This function only allowed to use for HTTP status "
242 "codes which explicitly defined in the RFC 7231 & 4918. "
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500243 "{0} is not a defined Success Code!"
244 ).format(expected_code)
245 if isinstance(expected_code, list):
246 for code in expected_code:
247 assert code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg
248 else:
249 assert expected_code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg
250
251 # NOTE(afazekas): the http status code above 400 is processed by
252 # the _error_checker method
253 if read_code < 400:
zhufl4d2f5152017-01-17 11:16:12 +0800254 pattern = ("Unexpected http success status code {0}, "
255 "The expected status code is {1}")
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500256 if ((not isinstance(expected_code, list) and
257 (read_code != expected_code)) or
258 (isinstance(expected_code, list) and
259 (read_code not in expected_code))):
260 details = pattern.format(read_code, expected_code)
261 raise exceptions.InvalidHttpSuccessCode(details)
262
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200263 def post(self, url, body, headers=None, extra_headers=False,
264 chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500265 """Send a HTTP POST request using keystone auth
266
267 :param str url: the relative url to send the post request to
268 :param dict body: the request body
269 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300270 :param bool extra_headers: Boolean value than indicates if the headers
271 returned by the get_headers() method are to
272 be used but additional headers are needed in
273 the request pass them in as a dict.
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200274 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500275 :return: a tuple with the first entry containing the response headers
276 and the second the response body
277 :rtype: tuple
278 """
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200279 return self.request('POST', url, extra_headers, headers, body, chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500280
281 def get(self, url, headers=None, extra_headers=False):
282 """Send a HTTP GET request using keystone service catalog and auth
283
284 :param str url: the relative url to send the post request to
285 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300286 :param bool extra_headers: Boolean value than indicates if the headers
287 returned by the get_headers() method are to
288 be used but additional headers are needed in
289 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500290 :return: a tuple with the first entry containing the response headers
291 and the second the response body
292 :rtype: tuple
293 """
294 return self.request('GET', url, extra_headers, headers)
295
296 def delete(self, url, headers=None, body=None, extra_headers=False):
297 """Send a HTTP DELETE request using keystone service catalog and auth
298
299 :param str url: the relative url to send the post request to
300 :param dict headers: The headers to use for the request
301 :param dict body: the request body
vsaienko9eb846b2016-04-09 00:35:47 +0300302 :param bool extra_headers: Boolean value than indicates if the headers
303 returned by the get_headers() method are to
304 be used but additional headers are needed in
305 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500306 :return: a tuple with the first entry containing the response headers
307 and the second the response body
308 :rtype: tuple
309 """
310 return self.request('DELETE', url, extra_headers, headers, body)
311
312 def patch(self, url, body, headers=None, extra_headers=False):
313 """Send a HTTP PATCH request using keystone service catalog and auth
314
315 :param str url: the relative url to send the post request to
316 :param dict body: the request body
317 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300318 :param bool extra_headers: Boolean value than indicates if the headers
319 returned by the get_headers() method are to
320 be used but additional headers are needed in
321 the request pass them in as a dict.
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 """
326 return self.request('PATCH', url, extra_headers, headers, body)
327
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200328 def put(self, url, body, headers=None, extra_headers=False, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500329 """Send a HTTP PUT request using keystone service catalog and auth
330
331 :param str url: the relative url to send the post request to
332 :param dict body: the request body
333 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300334 :param bool extra_headers: Boolean value than indicates if the headers
335 returned by the get_headers() method are to
336 be used but additional headers are needed in
337 the request pass them in as a dict.
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200338 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500339 :return: a tuple with the first entry containing the response headers
340 and the second the response body
341 :rtype: tuple
342 """
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200343 return self.request('PUT', url, extra_headers, headers, body, chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500344
345 def head(self, url, headers=None, extra_headers=False):
346 """Send a HTTP HEAD request using keystone service catalog and auth
347
348 :param str url: the relative url to send the post request to
349 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300350 :param bool extra_headers: Boolean value than indicates if the headers
351 returned by the get_headers() method are to
352 be used but additional headers are needed in
353 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500354 :return: a tuple with the first entry containing the response headers
355 and the second the response body
356 :rtype: tuple
357 """
358 return self.request('HEAD', url, extra_headers, headers)
359
360 def copy(self, url, headers=None, extra_headers=False):
361 """Send a HTTP COPY request using keystone service catalog and auth
362
363 :param str url: the relative url to send the post request to
364 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300365 :param bool extra_headers: Boolean value than indicates if the headers
366 returned by the get_headers() method are to
367 be used but additional headers are needed in
368 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500369 :return: a tuple with the first entry containing the response headers
370 and the second the response body
371 :rtype: tuple
372 """
373 return self.request('COPY', url, extra_headers, headers)
374
375 def get_versions(self):
376 """Get the versions on a endpoint from the keystone catalog
377
378 This method will make a GET request on the baseurl from the keystone
379 catalog to return a list of API versions. It is expected that a GET
380 on the endpoint in the catalog will return a list of supported API
381 versions.
382
junboli872ca872017-07-21 13:24:38 +0800383 :return: tuple with response headers and list of version numbers
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500384 :rtype: tuple
385 """
386 resp, body = self.get('')
387 body = self._parse_resp(body)
388 versions = map(lambda x: x['id'], body)
389 return resp, versions
390
391 def _get_request_id(self, resp):
392 for i in ('x-openstack-request-id', 'x-compute-request-id'):
393 if i in resp:
394 return resp[i]
395 return ""
396
397 def _safe_body(self, body, maxlen=4096):
398 # convert a structure into a string safely
399 try:
400 text = six.text_type(body)
401 except UnicodeDecodeError:
402 # if this isn't actually text, return marker that
403 return "<BinaryData: removed>"
404 if len(text) > maxlen:
405 return text[:maxlen]
406 else:
407 return text
408
guo yunxian9f749f92016-08-25 10:55:04 +0800409 def _log_request_start(self, method, req_url):
Jordan Pittier9e227c52016-02-09 14:35:18 +0100410 caller_name = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500411 if self.trace_requests and re.search(self.trace_requests, caller_name):
Jordan Pittier525ec712016-12-07 17:51:26 +0100412 self.LOG.debug('Starting Request (%s): %s %s', caller_name,
413 method, req_url)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500414
guo yunxian9f749f92016-08-25 10:55:04 +0800415 def _log_request_full(self, resp, req_headers=None, req_body=None,
416 resp_body=None, extra=None):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500417 if 'X-Auth-Token' in req_headers:
418 req_headers['X-Auth-Token'] = '<omitted>'
Ken'ichi Ohmichi2902a7b2018-07-14 02:31:03 +0000419 if 'X-Subject-Token' in req_headers:
420 req_headers['X-Subject-Token'] = '<omitted>'
Andrea Frittoli (andreaf)a1edb2d2016-05-10 16:09:59 +0100421 # A shallow copy is sufficient
422 resp_log = resp.copy()
423 if 'x-subject-token' in resp_log:
424 resp_log['x-subject-token'] = '<omitted>'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500425 log_fmt = """Request - Headers: %s
426 Body: %s
427 Response - Headers: %s
428 Body: %s"""
429
430 self.LOG.debug(
Jordan Pittier525ec712016-12-07 17:51:26 +0100431 log_fmt,
432 str(req_headers),
433 self._safe_body(req_body),
434 str(resp_log),
435 self._safe_body(resp_body),
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500436 extra=extra)
437
438 def _log_request(self, method, req_url, resp,
439 secs="", req_headers=None,
440 req_body=None, resp_body=None):
441 if req_headers is None:
442 req_headers = {}
443 # if we have the request id, put it in the right part of the log
444 extra = dict(request_id=self._get_request_id(resp))
445 # NOTE(sdague): while we still have 6 callers to this function
446 # we're going to just provide work around on who is actually
447 # providing timings by gracefully adding no content if they don't.
448 # Once we're down to 1 caller, clean this up.
Jordan Pittier9e227c52016-02-09 14:35:18 +0100449 caller_name = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500450 if secs:
451 secs = " %.3fs" % secs
452 self.LOG.info(
Jordan Pittier525ec712016-12-07 17:51:26 +0100453 'Request (%s): %s %s %s%s',
454 caller_name,
455 resp['status'],
456 method,
457 req_url,
458 secs,
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500459 extra=extra)
460
461 # Also look everything at DEBUG if you want to filter this
462 # out, don't run at debug.
Anusha Raminenif3eb9472017-01-13 08:54:01 +0530463 if self.LOG.isEnabledFor(logging.DEBUG):
guo yunxian9f749f92016-08-25 10:55:04 +0800464 self._log_request_full(resp, req_headers, req_body,
465 resp_body, extra)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500466
467 def _parse_resp(self, body):
468 try:
469 body = json.loads(body)
470 except ValueError:
471 return body
472
473 # We assume, that if the first value of the deserialized body's
474 # item set is a dict or a list, that we just return the first value
475 # of deserialized body.
476 # Essentially "cutting out" the first placeholder element in a body
477 # that looks like this:
478 #
479 # {
480 # "users": [
481 # ...
482 # ]
483 # }
484 try:
485 # Ensure there are not more than one top-level keys
486 # NOTE(freerunner): Ensure, that JSON is not nullable to
487 # to prevent StopIteration Exception
Ken'ichi Ohmichi69a8edc2017-04-28 11:41:20 -0700488 if not hasattr(body, "keys") or len(body.keys()) != 1:
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500489 return body
490 # Just return the "wrapped" element
491 first_key, first_item = six.next(six.iteritems(body))
492 if isinstance(first_item, (dict, list)):
493 return first_item
494 except (ValueError, IndexError):
495 pass
496 return body
497
498 def response_checker(self, method, resp, resp_body):
499 """A sanity check on the response from a HTTP request
500
501 This method does a sanity check on whether the response from an HTTP
502 request conforms the HTTP RFC.
503
504 :param str method: The HTTP verb of the request associated with the
505 response being passed in.
506 :param resp: The response headers
507 :param resp_body: The body of the response
508 :raises ResponseWithNonEmptyBody: If the response with the status code
509 is not supposed to have a body
510 :raises ResponseWithEntity: If the response code is 205 but has an
511 entity
512 """
513 if (resp.status in set((204, 205, 304)) or resp.status < 200 or
514 method.upper() == 'HEAD') and resp_body:
515 raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
516 # NOTE(afazekas):
517 # If the HTTP Status Code is 205
518 # 'The response MUST NOT include an entity.'
519 # A HTTP entity has an entity-body and an 'entity-header'.
520 # In the HTTP response specification (Section 6) the 'entity-header'
521 # 'generic-header' and 'response-header' are in OR relation.
522 # All headers not in the above two group are considered as entity
523 # header in every interpretation.
524
525 if (resp.status == 205 and
526 0 != len(set(resp.keys()) - set(('status',)) -
527 self.response_header_lc - self.general_header_lc)):
528 raise exceptions.ResponseWithEntity()
529 # NOTE(afazekas)
530 # Now the swift sometimes (delete not empty container)
531 # returns with non json error response, we can create new rest class
532 # for swift.
533 # Usually RFC2616 says error responses SHOULD contain an explanation.
534 # The warning is normal for SHOULD/SHOULD NOT case
535
536 # Likely it will cause an error
537 if method != 'HEAD' and not resp_body and resp.status >= 400:
538 self.LOG.warning("status >= 400 response with empty body")
539
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200540 def _request(self, method, url, headers=None, body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500541 """A simple HTTP request interface."""
542 # Authenticate the request with the auth provider
543 req_url, req_headers, req_body = self.auth_provider.auth_request(
544 method, url, headers, body, self.filters)
545
546 # Do the actual request, and time it
547 start = time.time()
548 self._log_request_start(method, req_url)
549 resp, resp_body = self.raw_request(
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200550 req_url, method, headers=req_headers, body=req_body,
551 chunked=chunked
552 )
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500553 end = time.time()
554 self._log_request(method, req_url, resp, secs=(end - start),
555 req_headers=req_headers, req_body=req_body,
556 resp_body=resp_body)
557
558 # Verify HTTP response codes
559 self.response_checker(method, resp, resp_body)
560
561 return resp, resp_body
562
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200563 def raw_request(self, url, method, headers=None, body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500564 """Send a raw HTTP request without the keystone catalog or auth
565
566 This method sends a HTTP request in the same manner as the request()
567 method, however it does so without using keystone auth or the catalog
568 to determine the base url. Additionally no response handling is done
569 the results from the request are just returned.
570
571 :param str url: Full url to send the request
572 :param str method: The HTTP verb to use for the request
zhuflcf35f652018-08-17 10:13:43 +0800573 :param dict headers: Headers to use for the request. If none are
574 specified, then the headers returned from the
575 get_headers() method are used. If the request
576 explicitly requires no headers use an empty dict.
Anh Trand44a8be2016-03-25 09:49:14 +0700577 :param str body: Body to send with the request
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200578 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500579 :rtype: tuple
580 :return: a tuple with the first entry containing the response headers
581 and the second the response body
582 """
583 if headers is None:
584 headers = self.get_headers()
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200585 return self.http_obj.request(url, method, headers=headers,
586 body=body, chunked=chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500587
588 def request(self, method, url, extra_headers=False, headers=None,
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200589 body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500590 """Send a HTTP request with keystone auth and using the catalog
591
592 This method will send an HTTP request using keystone auth in the
593 headers and the catalog to determine the endpoint to use for the
594 baseurl to send the request to. Additionally
595
596 When a response is received it will check it to see if an error
597 response was received. If it was an exception will be raised to enable
598 it to be handled quickly.
599
600 This method will also handle rate-limiting, if a 413 response code is
601 received it will retry the request after waiting the 'retry-after'
602 duration from the header.
603
604 :param str method: The HTTP verb to use for the request
605 :param str url: Relative url to send the request to
vsaienko9eb846b2016-04-09 00:35:47 +0300606 :param bool extra_headers: Boolean value than indicates if the headers
607 returned by the get_headers() method are to
608 be used but additional headers are needed in
609 the request pass them in as a dict.
zhuflcf35f652018-08-17 10:13:43 +0800610 :param dict headers: Headers to use for the request. If none are
611 specified, then the headers returned from the
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500612 get_headers() method are used. If the request
613 explicitly requires no headers use an empty dict.
Anh Trand44a8be2016-03-25 09:49:14 +0700614 :param str body: Body to send with the request
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200615 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500616 :rtype: tuple
617 :return: a tuple with the first entry containing the response headers
618 and the second the response body
619 :raises UnexpectedContentType: If the content-type of the response
620 isn't an expect type
621 :raises Unauthorized: If a 401 response code is received
622 :raises Forbidden: If a 403 response code is received
623 :raises NotFound: If a 404 response code is received
624 :raises BadRequest: If a 400 response code is received
625 :raises Gone: If a 410 response code is received
626 :raises Conflict: If a 409 response code is received
Kevin Bentona82bc862017-02-13 01:16:13 -0800627 :raises PreconditionFailed: If a 412 response code is received
zhuflcf35f652018-08-17 10:13:43 +0800628 :raises OverLimit: If a 413 response code is received and retry-after
629 is not in the response body or its retry operation
630 exceeds the limits defined by the server
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500631 :raises RateLimitExceeded: If a 413 response code is received and
zhuflcf35f652018-08-17 10:13:43 +0800632 retry-after is in the response body and
633 its retry operation does not exceeds the
634 limits defined by the server
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500635 :raises InvalidContentType: If a 415 response code is received
636 :raises UnprocessableEntity: If a 422 response code is received
637 :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
638 and couldn't be parsed
639 :raises NotImplemented: If a 501 response code is received
640 :raises ServerFault: If a 500 response code is received
641 :raises UnexpectedResponseCode: If a response code above 400 is
642 received and it doesn't fall into any
643 of the handled checks
644 """
645 # if extra_headers is True
646 # default headers would be added to headers
647 retry = 0
648
649 if headers is None:
650 # NOTE(vponomaryov): if some client do not need headers,
651 # it should explicitly pass empty dict
652 headers = self.get_headers()
653 elif extra_headers:
654 try:
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500655 headers.update(self.get_headers())
656 except (ValueError, TypeError):
657 headers = self.get_headers()
658
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200659 resp, resp_body = self._request(method, url, headers=headers,
660 body=body, chunked=chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500661
662 while (resp.status == 413 and
663 'retry-after' in resp and
664 not self.is_absolute_limit(
665 resp, self._parse_resp(resp_body)) and
666 retry < MAX_RECURSION_DEPTH):
667 retry += 1
Paul Glass119565a2016-04-06 11:41:42 -0500668 delay = self._get_retry_after_delay(resp)
669 self.LOG.debug(
670 "Sleeping %s seconds based on retry-after header", delay
671 )
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500672 time.sleep(delay)
673 resp, resp_body = self._request(method, url,
674 headers=headers, body=body)
Ken'ichi Ohmichi17051e82016-10-20 11:46:06 -0700675 self._error_checker(resp, resp_body)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500676 return resp, resp_body
677
Paul Glass119565a2016-04-06 11:41:42 -0500678 def _get_retry_after_delay(self, resp):
679 """Extract the delay from the retry-after header.
680
681 This supports both integer and HTTP date formatted retry-after headers
682 per RFC 2616.
683
684 :param resp: The response containing the retry-after headers
685 :rtype: int
686 :return: The delay in seconds, clamped to be at least 1 second
687 :raises ValueError: On failing to parse the delay
688 """
689 delay = None
690 try:
691 delay = int(resp['retry-after'])
692 except (ValueError, KeyError):
693 pass
694
695 try:
696 retry_timestamp = self._parse_http_date(resp['retry-after'])
697 date_timestamp = self._parse_http_date(resp['date'])
698 delay = int(retry_timestamp - date_timestamp)
699 except (ValueError, OverflowError, KeyError):
700 pass
701
702 if delay is None:
703 raise ValueError(
704 "Failed to parse retry-after header %r as either int or "
705 "HTTP-date." % resp.get('retry-after')
706 )
707
708 # Retry-after headers do not have sub-second precision. Clients may
709 # receive a delay of 0. After sleeping 0 seconds, we would (likely) hit
710 # another 413. To avoid this, always sleep at least 1 second.
711 return max(1, delay)
712
713 def _parse_http_date(self, val):
714 """Parse an HTTP date, like 'Fri, 31 Dec 1999 23:59:59 GMT'.
715
716 Return an epoch timestamp (float), as returned by time.mktime().
717 """
718 parts = email.utils.parsedate(val)
719 if not parts:
720 raise ValueError("Failed to parse date %s" % val)
721 return time.mktime(parts)
722
Ken'ichi Ohmichi17051e82016-10-20 11:46:06 -0700723 def _error_checker(self, resp, resp_body):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500724
725 # NOTE(mtreinish): Check for httplib response from glance_http. The
726 # object can't be used here because importing httplib breaks httplib2.
727 # If another object from a class not imported were passed here as
728 # resp this could possibly fail
729 if str(type(resp)) == "<type 'instance'>":
730 ctype = resp.getheader('content-type')
731 else:
732 try:
733 ctype = resp['content-type']
734 # NOTE(mtreinish): Keystone delete user responses doesn't have a
735 # content-type header. (They don't have a body) So just pretend it
736 # is set.
737 except KeyError:
738 ctype = 'application/json'
739
740 # It is not an error response
741 if resp.status < 400:
742 return
743
zhipenghd1db0c72017-02-21 04:40:07 -0500744 # NOTE(zhipengh): There is a purposefully duplicate of content-type
745 # with the only difference is with or without spaces, as specified
746 # in RFC7231.
747 JSON_ENC = ['application/json', 'application/json; charset=utf-8',
748 'application/json;charset=utf-8']
749
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500750 # NOTE(mtreinish): This is for compatibility with Glance and swift
751 # APIs. These are the return content types that Glance api v1
752 # (and occasionally swift) are using.
zhipenghd1db0c72017-02-21 04:40:07 -0500753 # NOTE(zhipengh): There is a purposefully duplicate of content-type
754 # with the only difference is with or without spaces, as specified
755 # in RFC7231.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500756 TXT_ENC = ['text/plain', 'text/html', 'text/html; charset=utf-8',
zhipenghd1db0c72017-02-21 04:40:07 -0500757 'text/plain; charset=utf-8', 'text/html;charset=utf-8',
758 'text/plain;charset=utf-8']
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500759
760 if ctype.lower() in JSON_ENC:
761 parse_resp = True
762 elif ctype.lower() in TXT_ENC:
763 parse_resp = False
764 else:
765 raise exceptions.UnexpectedContentType(str(resp.status),
766 resp=resp)
767
768 if resp.status == 401:
769 if parse_resp:
770 resp_body = self._parse_resp(resp_body)
771 raise exceptions.Unauthorized(resp_body, resp=resp)
772
773 if resp.status == 403:
774 if parse_resp:
775 resp_body = self._parse_resp(resp_body)
776 raise exceptions.Forbidden(resp_body, resp=resp)
777
778 if resp.status == 404:
779 if parse_resp:
780 resp_body = self._parse_resp(resp_body)
781 raise exceptions.NotFound(resp_body, resp=resp)
782
783 if resp.status == 400:
784 if parse_resp:
785 resp_body = self._parse_resp(resp_body)
786 raise exceptions.BadRequest(resp_body, resp=resp)
787
788 if resp.status == 410:
789 if parse_resp:
790 resp_body = self._parse_resp(resp_body)
791 raise exceptions.Gone(resp_body, resp=resp)
792
793 if resp.status == 409:
794 if parse_resp:
795 resp_body = self._parse_resp(resp_body)
796 raise exceptions.Conflict(resp_body, resp=resp)
797
Kevin Bentona82bc862017-02-13 01:16:13 -0800798 if resp.status == 412:
799 if parse_resp:
800 resp_body = self._parse_resp(resp_body)
801 raise exceptions.PreconditionFailed(resp_body, resp=resp)
802
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500803 if resp.status == 413:
804 if parse_resp:
805 resp_body = self._parse_resp(resp_body)
806 if self.is_absolute_limit(resp, resp_body):
807 raise exceptions.OverLimit(resp_body, resp=resp)
808 else:
809 raise exceptions.RateLimitExceeded(resp_body, resp=resp)
810
811 if resp.status == 415:
812 if parse_resp:
813 resp_body = self._parse_resp(resp_body)
814 raise exceptions.InvalidContentType(resp_body, resp=resp)
815
816 if resp.status == 422:
817 if parse_resp:
818 resp_body = self._parse_resp(resp_body)
819 raise exceptions.UnprocessableEntity(resp_body, resp=resp)
820
821 if resp.status in (500, 501):
822 message = resp_body
823 if parse_resp:
824 try:
825 resp_body = self._parse_resp(resp_body)
826 except ValueError:
827 # If response body is a non-json string message.
828 # Use resp_body as is and raise InvalidResponseBody
829 # exception.
830 raise exceptions.InvalidHTTPResponseBody(message)
831 else:
832 if isinstance(resp_body, dict):
833 # I'm seeing both computeFault
834 # and cloudServersFault come back.
835 # Will file a bug to fix, but leave as is for now.
836 if 'cloudServersFault' in resp_body:
837 message = resp_body['cloudServersFault']['message']
838 elif 'computeFault' in resp_body:
839 message = resp_body['computeFault']['message']
840 elif 'error' in resp_body:
841 message = resp_body['error']['message']
842 elif 'message' in resp_body:
843 message = resp_body['message']
844 else:
845 message = resp_body
846
847 if resp.status == 501:
848 raise exceptions.NotImplemented(resp_body, resp=resp,
849 message=message)
850 else:
851 raise exceptions.ServerFault(resp_body, resp=resp,
852 message=message)
853
854 if resp.status >= 400:
855 raise exceptions.UnexpectedResponseCode(str(resp.status),
856 resp=resp)
857
858 def is_absolute_limit(self, resp, resp_body):
859 if (not isinstance(resp_body, collections.Mapping) or
860 'retry-after' not in resp):
861 return True
Paul Glass119565a2016-04-06 11:41:42 -0500862 return 'exceed' in resp_body.get('message', 'blabla')
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500863
864 def wait_for_resource_deletion(self, id):
865 """Waits for a resource to be deleted
866
867 This method will loop over is_resource_deleted until either
868 is_resource_deleted returns True or the build timeout is reached. This
869 depends on is_resource_deleted being implemented
870
871 :param str id: The id of the resource to check
872 :raises TimeoutException: If the build_timeout has elapsed and the
873 resource still hasn't been deleted
874 """
875 start_time = int(time.time())
876 while True:
877 if self.is_resource_deleted(id):
878 return
879 if int(time.time()) - start_time >= self.build_timeout:
880 message = ('Failed to delete %(resource_type)s %(id)s within '
881 'the required time (%(timeout)s s).' %
882 {'resource_type': self.resource_type, 'id': id,
883 'timeout': self.build_timeout})
Jordan Pittier9e227c52016-02-09 14:35:18 +0100884 caller = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500885 if caller:
886 message = '(%s) %s' % (caller, message)
887 raise exceptions.TimeoutException(message)
888 time.sleep(self.build_interval)
889
890 def is_resource_deleted(self, id):
891 """Subclasses override with specific deletion detection."""
892 message = ('"%s" does not implement is_resource_deleted'
893 % self.__class__.__name__)
894 raise NotImplementedError(message)
895
896 @property
897 def resource_type(self):
898 """Returns the primary type of resource this client works with."""
899 return 'resource'
900
901 @classmethod
902 def validate_response(cls, schema, resp, body):
903 # Only check the response if the status code is a success code
904 # TODO(cyeoh): Eventually we should be able to verify that a failure
905 # code if it exists is something that we expect. This is explicitly
906 # declared in the V3 API and so we should be able to export this in
907 # the response schema. For now we'll ignore it.
908 if resp.status in HTTP_SUCCESS + HTTP_REDIRECTION:
909 cls.expected_success(schema['status_code'], resp.status)
910
911 # Check the body of a response
912 body_schema = schema.get('response_body')
913 if body_schema:
914 try:
915 jsonschema.validate(body, body_schema,
916 cls=JSONSCHEMA_VALIDATOR,
917 format_checker=FORMAT_CHECKER)
918 except jsonschema.ValidationError as ex:
guo yunxiana3f55282016-08-10 14:35:16 +0800919 msg = ("HTTP response body is invalid (%s)" % ex)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500920 raise exceptions.InvalidHTTPResponseBody(msg)
921 else:
922 if body:
guo yunxiana3f55282016-08-10 14:35:16 +0800923 msg = ("HTTP response body should not exist (%s)" % body)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500924 raise exceptions.InvalidHTTPResponseBody(msg)
925
926 # Check the header of a response
927 header_schema = schema.get('response_header')
928 if header_schema:
929 try:
930 jsonschema.validate(resp, header_schema,
931 cls=JSONSCHEMA_VALIDATOR,
932 format_checker=FORMAT_CHECKER)
933 except jsonschema.ValidationError as ex:
guo yunxiana3f55282016-08-10 14:35:16 +0800934 msg = ("HTTP response header is invalid (%s)" % ex)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500935 raise exceptions.InvalidHTTPResponseHeader(msg)
936
Ken'ichi Ohmichida26b162017-03-03 15:53:46 -0800937 def _get_base_version_url(self):
938 # TODO(oomichi): This method can be used for auth's replace_version().
939 # So it is nice to have common logic for the maintenance.
940 endpoint = self.base_url
941 url = urllib.parse.urlsplit(endpoint)
942 new_path = re.split(r'(^|/)+v\d+(\.\d+)?', url.path)[0]
943 url = list(url)
944 url[2] = new_path + '/'
945 return urllib.parse.urlunsplit(url)
946
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500947
948class ResponseBody(dict):
949 """Class that wraps an http response and dict body into a single value.
950
951 Callers that receive this object will normally use it as a dict but
952 can extract the response if needed.
953 """
954
955 def __init__(self, response, body=None):
956 body_data = body or {}
957 self.update(body_data)
958 self.response = response
959
960 def __str__(self):
961 body = super(ResponseBody, self).__str__()
962 return "response: %s\nBody: %s" % (self.response, body)
963
964
965class ResponseBodyData(object):
966 """Class that wraps an http response and string data into a single value.
967
968 """
969
970 def __init__(self, response, data):
971 self.response = response
972 self.data = data
973
974 def __str__(self):
975 return "response: %s\nBody: %s" % (self.response, self.data)
976
977
978class ResponseBodyList(list):
979 """Class that wraps an http response and list body into a single value.
980
981 Callers that receive this object will normally use it as a list but
982 can extract the response if needed.
983 """
984
985 def __init__(self, response, body=None):
986 body_data = body or []
987 self.extend(body_data)
988 self.response = response
989
990 def __str__(self):
991 body = super(ResponseBodyList, self).__str__()
992 return "response: %s\nBody: %s" % (self.response, body)