blob: a6f5570f261a9eef1271d213d2b3fa3f49fa012e [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.
Jens Harbott3ffa54e2018-07-04 11:59:49 +000073 :param bool follow_redirects: Set to false to stop following redirects.
Matthew Treinish9e26ca82016-02-23 11:43:20 -050074 """
Matthew Treinish9e26ca82016-02-23 11:43:20 -050075
76 # The version of the API this client implements
77 api_version = None
78
79 LOG = logging.getLogger(__name__)
80
81 def __init__(self, auth_provider, service, region,
82 endpoint_type='publicURL',
83 build_interval=1, build_timeout=60,
84 disable_ssl_certificate_validation=False, ca_certs=None,
Matthew Treinish74514402016-09-01 11:44:57 -040085 trace_requests='', name=None, http_timeout=None,
Jens Harbott3ffa54e2018-07-04 11:59:49 +000086 proxy_url=None, follow_redirects=True):
Matthew Treinish9e26ca82016-02-23 11:43:20 -050087 self.auth_provider = auth_provider
88 self.service = service
89 self.region = region
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -060090 self.name = name
Matthew Treinish9e26ca82016-02-23 11:43:20 -050091 self.endpoint_type = endpoint_type
92 self.build_interval = build_interval
93 self.build_timeout = build_timeout
94 self.trace_requests = trace_requests
95
96 self._skip_path = False
97 self.general_header_lc = set(('cache-control', 'connection',
98 'date', 'pragma', 'trailer',
99 'transfer-encoding', 'via',
100 'warning'))
101 self.response_header_lc = set(('accept-ranges', 'age', 'etag',
102 'location', 'proxy-authenticate',
103 'retry-after', 'server',
104 'vary', 'www-authenticate'))
105 dscv = disable_ssl_certificate_validation
Matthew Treinish74514402016-09-01 11:44:57 -0400106
107 if proxy_url:
108 self.http_obj = http.ClosingProxyHttp(
109 proxy_url,
110 disable_ssl_certificate_validation=dscv, ca_certs=ca_certs,
Jens Harbott3ffa54e2018-07-04 11:59:49 +0000111 timeout=http_timeout, follow_redirects=follow_redirects)
Matthew Treinish74514402016-09-01 11:44:57 -0400112 else:
113 self.http_obj = http.ClosingHttp(
114 disable_ssl_certificate_validation=dscv, ca_certs=ca_certs,
Jens Harbott3ffa54e2018-07-04 11:59:49 +0000115 timeout=http_timeout, follow_redirects=follow_redirects)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500116
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500117 def get_headers(self, accept_type=None, send_type=None):
118 """Return the default headers which will be used with outgoing requests
119
120 :param str accept_type: The media type to use for the Accept header, if
121 one isn't provided the object var TYPE will be
122 used
123 :param str send_type: The media-type to use for the Content-Type
124 header, if one isn't provided the object var
125 TYPE will be used
126 :rtype: dict
127 :return: The dictionary of headers which can be used in the headers
128 dict for outgoing request
129 """
130 if accept_type is None:
Masayuki Igawa189b92f2017-04-24 18:57:17 +0900131 accept_type = 'json'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500132 if send_type is None:
Masayuki Igawa189b92f2017-04-24 18:57:17 +0900133 send_type = 'json'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500134 return {'Content-Type': 'application/%s' % send_type,
135 'Accept': 'application/%s' % accept_type}
136
137 def __str__(self):
138 STRING_LIMIT = 80
139 str_format = ("service:%s, base_url:%s, "
140 "filters: %s, build_interval:%s, build_timeout:%s"
141 "\ntoken:%s..., \nheaders:%s...")
142 return str_format % (self.service, self.base_url,
143 self.filters, self.build_interval,
144 self.build_timeout,
145 str(self.token)[0:STRING_LIMIT],
146 str(self.get_headers())[0:STRING_LIMIT])
147
148 @property
149 def user(self):
150 """The username used for requests
151
152 :rtype: string
153 :return: The username being used for requests
154 """
155
156 return self.auth_provider.credentials.username
157
158 @property
159 def user_id(self):
160 """The user_id used for requests
161
162 :rtype: string
163 :return: The user id being used for requests
164 """
165 return self.auth_provider.credentials.user_id
166
167 @property
168 def tenant_name(self):
169 """The tenant/project being used for requests
170
171 :rtype: string
172 :return: The tenant/project name being used for requests
173 """
174 return self.auth_provider.credentials.tenant_name
175
176 @property
177 def tenant_id(self):
178 """The tenant/project id being used for requests
179
180 :rtype: string
181 :return: The tenant/project id being used for requests
182 """
183 return self.auth_provider.credentials.tenant_id
184
185 @property
186 def password(self):
187 """The password being used for requests
188
189 :rtype: string
190 :return: The password being used for requests
191 """
192 return self.auth_provider.credentials.password
193
194 @property
195 def base_url(self):
196 return self.auth_provider.base_url(filters=self.filters)
197
198 @property
199 def token(self):
200 return self.auth_provider.get_token()
201
202 @property
203 def filters(self):
204 _filters = dict(
205 service=self.service,
206 endpoint_type=self.endpoint_type,
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -0600207 region=self.region,
208 name=self.name
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500209 )
210 if self.api_version is not None:
211 _filters['api_version'] = self.api_version
212 if self._skip_path:
213 _filters['skip_path'] = self._skip_path
214 return _filters
215
216 def skip_path(self):
217 """When set, ignore the path part of the base URL from the catalog"""
218 self._skip_path = True
219
220 def reset_path(self):
221 """When reset, use the base URL from the catalog as-is"""
222 self._skip_path = False
223
224 @classmethod
225 def expected_success(cls, expected_code, read_code):
226 """Check expected success response code against the http response
227
228 :param int expected_code: The response code that is expected.
229 Optionally a list of integers can be used
230 to specify multiple valid success codes
231 :param int read_code: The response code which was returned in the
232 response
233 :raises AssertionError: if the expected_code isn't a valid http success
234 response code
235 :raises exceptions.InvalidHttpSuccessCode: if the read code isn't an
236 expected http success code
237 """
ghanshyamc3074202016-04-18 15:20:45 +0900238 if not isinstance(read_code, int):
239 raise TypeError("'read_code' must be an int instead of (%s)"
240 % type(read_code))
241
Hanxi2f977db2016-09-01 17:31:28 +0800242 assert_msg = ("This function only allowed to use for HTTP status "
243 "codes which explicitly defined in the RFC 7231 & 4918. "
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500244 "{0} is not a defined Success Code!"
245 ).format(expected_code)
246 if isinstance(expected_code, list):
247 for code in expected_code:
248 assert code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg
249 else:
250 assert expected_code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg
251
252 # NOTE(afazekas): the http status code above 400 is processed by
253 # the _error_checker method
254 if read_code < 400:
zhufl4d2f5152017-01-17 11:16:12 +0800255 pattern = ("Unexpected http success status code {0}, "
256 "The expected status code is {1}")
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500257 if ((not isinstance(expected_code, list) and
258 (read_code != expected_code)) or
259 (isinstance(expected_code, list) and
260 (read_code not in expected_code))):
261 details = pattern.format(read_code, expected_code)
262 raise exceptions.InvalidHttpSuccessCode(details)
263
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200264 def post(self, url, body, headers=None, extra_headers=False,
265 chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500266 """Send a HTTP POST request using keystone auth
267
268 :param str url: the relative url to send the post request to
269 :param dict body: the request body
270 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300271 :param bool extra_headers: Boolean value than indicates if the headers
272 returned by the get_headers() method are to
273 be used but additional headers are needed in
274 the request pass them in as a dict.
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200275 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500276 :return: a tuple with the first entry containing the response headers
277 and the second the response body
278 :rtype: tuple
279 """
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200280 return self.request('POST', url, extra_headers, headers, body, chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500281
282 def get(self, url, headers=None, extra_headers=False):
283 """Send a HTTP GET request using keystone service catalog and auth
284
285 :param str url: the relative url to send the post request to
286 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300287 :param bool extra_headers: Boolean value than indicates if the headers
288 returned by the get_headers() method are to
289 be used but additional headers are needed in
290 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500291 :return: a tuple with the first entry containing the response headers
292 and the second the response body
293 :rtype: tuple
294 """
295 return self.request('GET', url, extra_headers, headers)
296
297 def delete(self, url, headers=None, body=None, extra_headers=False):
298 """Send a HTTP DELETE request using keystone service catalog and auth
299
300 :param str url: the relative url to send the post request to
301 :param dict headers: The headers to use for the request
302 :param dict body: the request body
vsaienko9eb846b2016-04-09 00:35:47 +0300303 :param bool extra_headers: Boolean value than indicates if the headers
304 returned by the get_headers() method are to
305 be used but additional headers are needed in
306 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500307 :return: a tuple with the first entry containing the response headers
308 and the second the response body
309 :rtype: tuple
310 """
311 return self.request('DELETE', url, extra_headers, headers, body)
312
313 def patch(self, url, body, headers=None, extra_headers=False):
314 """Send a HTTP PATCH request using keystone service catalog and auth
315
316 :param str url: the relative url to send the post request to
317 :param dict body: the request body
318 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300319 :param bool extra_headers: Boolean value than indicates if the headers
320 returned by the get_headers() method are to
321 be used but additional headers are needed in
322 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500323 :return: a tuple with the first entry containing the response headers
324 and the second the response body
325 :rtype: tuple
326 """
327 return self.request('PATCH', url, extra_headers, headers, body)
328
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200329 def put(self, url, body, headers=None, extra_headers=False, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500330 """Send a HTTP PUT request using keystone service catalog and auth
331
332 :param str url: the relative url to send the post request to
333 :param dict body: the request body
334 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300335 :param bool extra_headers: Boolean value than indicates if the headers
336 returned by the get_headers() method are to
337 be used but additional headers are needed in
338 the request pass them in as a dict.
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200339 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500340 :return: a tuple with the first entry containing the response headers
341 and the second the response body
342 :rtype: tuple
343 """
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200344 return self.request('PUT', url, extra_headers, headers, body, chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500345
346 def head(self, url, headers=None, extra_headers=False):
347 """Send a HTTP HEAD request using keystone service catalog and auth
348
349 :param str url: the relative url to send the post request to
350 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300351 :param bool extra_headers: Boolean value than indicates if the headers
352 returned by the get_headers() method are to
353 be used but additional headers are needed in
354 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500355 :return: a tuple with the first entry containing the response headers
356 and the second the response body
357 :rtype: tuple
358 """
359 return self.request('HEAD', url, extra_headers, headers)
360
361 def copy(self, url, headers=None, extra_headers=False):
362 """Send a HTTP COPY request using keystone service catalog and auth
363
364 :param str url: the relative url to send the post request to
365 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300366 :param bool extra_headers: Boolean value than indicates if the headers
367 returned by the get_headers() method are to
368 be used but additional headers are needed in
369 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500370 :return: a tuple with the first entry containing the response headers
371 and the second the response body
372 :rtype: tuple
373 """
374 return self.request('COPY', url, extra_headers, headers)
375
376 def get_versions(self):
377 """Get the versions on a endpoint from the keystone catalog
378
379 This method will make a GET request on the baseurl from the keystone
380 catalog to return a list of API versions. It is expected that a GET
381 on the endpoint in the catalog will return a list of supported API
382 versions.
383
junboli872ca872017-07-21 13:24:38 +0800384 :return: tuple with response headers and list of version numbers
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500385 :rtype: tuple
386 """
387 resp, body = self.get('')
388 body = self._parse_resp(body)
389 versions = map(lambda x: x['id'], body)
390 return resp, versions
391
392 def _get_request_id(self, resp):
393 for i in ('x-openstack-request-id', 'x-compute-request-id'):
394 if i in resp:
395 return resp[i]
396 return ""
397
398 def _safe_body(self, body, maxlen=4096):
399 # convert a structure into a string safely
400 try:
401 text = six.text_type(body)
402 except UnicodeDecodeError:
403 # if this isn't actually text, return marker that
404 return "<BinaryData: removed>"
405 if len(text) > maxlen:
406 return text[:maxlen]
407 else:
408 return text
409
guo yunxian9f749f92016-08-25 10:55:04 +0800410 def _log_request_start(self, method, req_url):
Jordan Pittier9e227c52016-02-09 14:35:18 +0100411 caller_name = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500412 if self.trace_requests and re.search(self.trace_requests, caller_name):
Jordan Pittier525ec712016-12-07 17:51:26 +0100413 self.LOG.debug('Starting Request (%s): %s %s', caller_name,
414 method, req_url)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500415
guo yunxian9f749f92016-08-25 10:55:04 +0800416 def _log_request_full(self, resp, req_headers=None, req_body=None,
417 resp_body=None, extra=None):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500418 if 'X-Auth-Token' in req_headers:
419 req_headers['X-Auth-Token'] = '<omitted>'
Ken'ichi Ohmichi2902a7b2018-07-14 02:31:03 +0000420 if 'X-Subject-Token' in req_headers:
421 req_headers['X-Subject-Token'] = '<omitted>'
Andrea Frittoli (andreaf)a1edb2d2016-05-10 16:09:59 +0100422 # A shallow copy is sufficient
423 resp_log = resp.copy()
424 if 'x-subject-token' in resp_log:
425 resp_log['x-subject-token'] = '<omitted>'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500426 log_fmt = """Request - Headers: %s
427 Body: %s
428 Response - Headers: %s
429 Body: %s"""
430
431 self.LOG.debug(
Jordan Pittier525ec712016-12-07 17:51:26 +0100432 log_fmt,
433 str(req_headers),
434 self._safe_body(req_body),
435 str(resp_log),
436 self._safe_body(resp_body),
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500437 extra=extra)
438
439 def _log_request(self, method, req_url, resp,
440 secs="", req_headers=None,
441 req_body=None, resp_body=None):
442 if req_headers is None:
443 req_headers = {}
444 # if we have the request id, put it in the right part of the log
445 extra = dict(request_id=self._get_request_id(resp))
446 # NOTE(sdague): while we still have 6 callers to this function
447 # we're going to just provide work around on who is actually
448 # providing timings by gracefully adding no content if they don't.
449 # Once we're down to 1 caller, clean this up.
Jordan Pittier9e227c52016-02-09 14:35:18 +0100450 caller_name = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500451 if secs:
452 secs = " %.3fs" % secs
453 self.LOG.info(
Jordan Pittier525ec712016-12-07 17:51:26 +0100454 'Request (%s): %s %s %s%s',
455 caller_name,
456 resp['status'],
457 method,
458 req_url,
459 secs,
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500460 extra=extra)
461
462 # Also look everything at DEBUG if you want to filter this
463 # out, don't run at debug.
Anusha Raminenif3eb9472017-01-13 08:54:01 +0530464 if self.LOG.isEnabledFor(logging.DEBUG):
guo yunxian9f749f92016-08-25 10:55:04 +0800465 self._log_request_full(resp, req_headers, req_body,
466 resp_body, extra)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500467
468 def _parse_resp(self, body):
469 try:
470 body = json.loads(body)
471 except ValueError:
472 return body
473
474 # We assume, that if the first value of the deserialized body's
475 # item set is a dict or a list, that we just return the first value
476 # of deserialized body.
477 # Essentially "cutting out" the first placeholder element in a body
478 # that looks like this:
479 #
480 # {
481 # "users": [
482 # ...
483 # ]
484 # }
485 try:
486 # Ensure there are not more than one top-level keys
487 # NOTE(freerunner): Ensure, that JSON is not nullable to
488 # to prevent StopIteration Exception
Ken'ichi Ohmichi69a8edc2017-04-28 11:41:20 -0700489 if not hasattr(body, "keys") or len(body.keys()) != 1:
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500490 return body
491 # Just return the "wrapped" element
492 first_key, first_item = six.next(six.iteritems(body))
493 if isinstance(first_item, (dict, list)):
494 return first_item
495 except (ValueError, IndexError):
496 pass
497 return body
498
499 def response_checker(self, method, resp, resp_body):
500 """A sanity check on the response from a HTTP request
501
502 This method does a sanity check on whether the response from an HTTP
503 request conforms the HTTP RFC.
504
505 :param str method: The HTTP verb of the request associated with the
506 response being passed in.
507 :param resp: The response headers
508 :param resp_body: The body of the response
509 :raises ResponseWithNonEmptyBody: If the response with the status code
510 is not supposed to have a body
511 :raises ResponseWithEntity: If the response code is 205 but has an
512 entity
513 """
514 if (resp.status in set((204, 205, 304)) or resp.status < 200 or
515 method.upper() == 'HEAD') and resp_body:
516 raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
517 # NOTE(afazekas):
518 # If the HTTP Status Code is 205
519 # 'The response MUST NOT include an entity.'
520 # A HTTP entity has an entity-body and an 'entity-header'.
521 # In the HTTP response specification (Section 6) the 'entity-header'
522 # 'generic-header' and 'response-header' are in OR relation.
523 # All headers not in the above two group are considered as entity
524 # header in every interpretation.
525
526 if (resp.status == 205 and
527 0 != len(set(resp.keys()) - set(('status',)) -
528 self.response_header_lc - self.general_header_lc)):
Matt Riedemann91d92422019-01-29 16:19:49 -0500529 raise exceptions.ResponseWithEntity()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500530 # NOTE(afazekas)
531 # Now the swift sometimes (delete not empty container)
532 # returns with non json error response, we can create new rest class
533 # for swift.
534 # Usually RFC2616 says error responses SHOULD contain an explanation.
535 # The warning is normal for SHOULD/SHOULD NOT case
536
537 # Likely it will cause an error
538 if method != 'HEAD' and not resp_body and resp.status >= 400:
539 self.LOG.warning("status >= 400 response with empty body")
540
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200541 def _request(self, method, url, headers=None, body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500542 """A simple HTTP request interface."""
543 # Authenticate the request with the auth provider
544 req_url, req_headers, req_body = self.auth_provider.auth_request(
545 method, url, headers, body, self.filters)
546
547 # Do the actual request, and time it
548 start = time.time()
549 self._log_request_start(method, req_url)
550 resp, resp_body = self.raw_request(
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200551 req_url, method, headers=req_headers, body=req_body,
552 chunked=chunked
553 )
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500554 end = time.time()
555 self._log_request(method, req_url, resp, secs=(end - start),
556 req_headers=req_headers, req_body=req_body,
557 resp_body=resp_body)
558
559 # Verify HTTP response codes
560 self.response_checker(method, resp, resp_body)
561
562 return resp, resp_body
563
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200564 def raw_request(self, url, method, headers=None, body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500565 """Send a raw HTTP request without the keystone catalog or auth
566
567 This method sends a HTTP request in the same manner as the request()
568 method, however it does so without using keystone auth or the catalog
569 to determine the base url. Additionally no response handling is done
570 the results from the request are just returned.
571
572 :param str url: Full url to send the request
573 :param str method: The HTTP verb to use for the request
zhuflcf35f652018-08-17 10:13:43 +0800574 :param dict headers: Headers to use for the request. If none are
575 specified, then the headers returned from the
576 get_headers() method are used. If the request
577 explicitly requires no headers use an empty dict.
Anh Trand44a8be2016-03-25 09:49:14 +0700578 :param str body: Body to send with the request
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200579 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500580 :rtype: tuple
581 :return: a tuple with the first entry containing the response headers
582 and the second the response body
583 """
584 if headers is None:
585 headers = self.get_headers()
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200586 return self.http_obj.request(url, method, headers=headers,
587 body=body, chunked=chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500588
589 def request(self, method, url, extra_headers=False, headers=None,
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200590 body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500591 """Send a HTTP request with keystone auth and using the catalog
592
593 This method will send an HTTP request using keystone auth in the
594 headers and the catalog to determine the endpoint to use for the
595 baseurl to send the request to. Additionally
596
597 When a response is received it will check it to see if an error
598 response was received. If it was an exception will be raised to enable
599 it to be handled quickly.
600
601 This method will also handle rate-limiting, if a 413 response code is
602 received it will retry the request after waiting the 'retry-after'
603 duration from the header.
604
605 :param str method: The HTTP verb to use for the request
606 :param str url: Relative url to send the request to
vsaienko9eb846b2016-04-09 00:35:47 +0300607 :param bool extra_headers: Boolean value than indicates if the headers
608 returned by the get_headers() method are to
609 be used but additional headers are needed in
610 the request pass them in as a dict.
zhuflcf35f652018-08-17 10:13:43 +0800611 :param dict headers: Headers to use for the request. If none are
612 specified, then the headers returned from the
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500613 get_headers() method are used. If the request
614 explicitly requires no headers use an empty dict.
Anh Trand44a8be2016-03-25 09:49:14 +0700615 :param str body: Body to send with the request
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200616 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500617 :rtype: tuple
618 :return: a tuple with the first entry containing the response headers
619 and the second the response body
620 :raises UnexpectedContentType: If the content-type of the response
621 isn't an expect type
622 :raises Unauthorized: If a 401 response code is received
623 :raises Forbidden: If a 403 response code is received
624 :raises NotFound: If a 404 response code is received
625 :raises BadRequest: If a 400 response code is received
626 :raises Gone: If a 410 response code is received
627 :raises Conflict: If a 409 response code is received
Kevin Bentona82bc862017-02-13 01:16:13 -0800628 :raises PreconditionFailed: If a 412 response code is received
zhuflcf35f652018-08-17 10:13:43 +0800629 :raises OverLimit: If a 413 response code is received and retry-after
630 is not in the response body or its retry operation
631 exceeds the limits defined by the server
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500632 :raises RateLimitExceeded: If a 413 response code is received and
zhuflcf35f652018-08-17 10:13:43 +0800633 retry-after is in the response body and
634 its retry operation does not exceeds the
635 limits defined by the server
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500636 :raises InvalidContentType: If a 415 response code is received
637 :raises UnprocessableEntity: If a 422 response code is received
638 :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
639 and couldn't be parsed
640 :raises NotImplemented: If a 501 response code is received
641 :raises ServerFault: If a 500 response code is received
642 :raises UnexpectedResponseCode: If a response code above 400 is
643 received and it doesn't fall into any
644 of the handled checks
645 """
646 # if extra_headers is True
647 # default headers would be added to headers
648 retry = 0
649
650 if headers is None:
651 # NOTE(vponomaryov): if some client do not need headers,
652 # it should explicitly pass empty dict
653 headers = self.get_headers()
654 elif extra_headers:
655 try:
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500656 headers.update(self.get_headers())
657 except (ValueError, TypeError):
658 headers = self.get_headers()
659
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200660 resp, resp_body = self._request(method, url, headers=headers,
661 body=body, chunked=chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500662
663 while (resp.status == 413 and
664 'retry-after' in resp and
665 not self.is_absolute_limit(
666 resp, self._parse_resp(resp_body)) and
667 retry < MAX_RECURSION_DEPTH):
668 retry += 1
Paul Glass119565a2016-04-06 11:41:42 -0500669 delay = self._get_retry_after_delay(resp)
670 self.LOG.debug(
671 "Sleeping %s seconds based on retry-after header", delay
672 )
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500673 time.sleep(delay)
674 resp, resp_body = self._request(method, url,
675 headers=headers, body=body)
Ken'ichi Ohmichi17051e82016-10-20 11:46:06 -0700676 self._error_checker(resp, resp_body)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500677 return resp, resp_body
678
Paul Glass119565a2016-04-06 11:41:42 -0500679 def _get_retry_after_delay(self, resp):
680 """Extract the delay from the retry-after header.
681
682 This supports both integer and HTTP date formatted retry-after headers
683 per RFC 2616.
684
685 :param resp: The response containing the retry-after headers
686 :rtype: int
687 :return: The delay in seconds, clamped to be at least 1 second
688 :raises ValueError: On failing to parse the delay
689 """
690 delay = None
691 try:
692 delay = int(resp['retry-after'])
693 except (ValueError, KeyError):
694 pass
695
696 try:
697 retry_timestamp = self._parse_http_date(resp['retry-after'])
698 date_timestamp = self._parse_http_date(resp['date'])
699 delay = int(retry_timestamp - date_timestamp)
700 except (ValueError, OverflowError, KeyError):
701 pass
702
703 if delay is None:
704 raise ValueError(
705 "Failed to parse retry-after header %r as either int or "
706 "HTTP-date." % resp.get('retry-after')
707 )
708
709 # Retry-after headers do not have sub-second precision. Clients may
710 # receive a delay of 0. After sleeping 0 seconds, we would (likely) hit
711 # another 413. To avoid this, always sleep at least 1 second.
712 return max(1, delay)
713
714 def _parse_http_date(self, val):
715 """Parse an HTTP date, like 'Fri, 31 Dec 1999 23:59:59 GMT'.
716
717 Return an epoch timestamp (float), as returned by time.mktime().
718 """
719 parts = email.utils.parsedate(val)
720 if not parts:
721 raise ValueError("Failed to parse date %s" % val)
722 return time.mktime(parts)
723
Ken'ichi Ohmichi17051e82016-10-20 11:46:06 -0700724 def _error_checker(self, resp, resp_body):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500725
726 # NOTE(mtreinish): Check for httplib response from glance_http. The
727 # object can't be used here because importing httplib breaks httplib2.
728 # If another object from a class not imported were passed here as
729 # resp this could possibly fail
730 if str(type(resp)) == "<type 'instance'>":
731 ctype = resp.getheader('content-type')
732 else:
733 try:
734 ctype = resp['content-type']
735 # NOTE(mtreinish): Keystone delete user responses doesn't have a
736 # content-type header. (They don't have a body) So just pretend it
737 # is set.
738 except KeyError:
739 ctype = 'application/json'
740
741 # It is not an error response
742 if resp.status < 400:
743 return
744
zhipenghd1db0c72017-02-21 04:40:07 -0500745 # NOTE(zhipengh): There is a purposefully duplicate of content-type
746 # with the only difference is with or without spaces, as specified
747 # in RFC7231.
748 JSON_ENC = ['application/json', 'application/json; charset=utf-8',
749 'application/json;charset=utf-8']
750
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500751 # NOTE(mtreinish): This is for compatibility with Glance and swift
752 # APIs. These are the return content types that Glance api v1
753 # (and occasionally swift) are using.
zhipenghd1db0c72017-02-21 04:40:07 -0500754 # NOTE(zhipengh): There is a purposefully duplicate of content-type
755 # with the only difference is with or without spaces, as specified
756 # in RFC7231.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500757 TXT_ENC = ['text/plain', 'text/html', 'text/html; charset=utf-8',
zhipenghd1db0c72017-02-21 04:40:07 -0500758 'text/plain; charset=utf-8', 'text/html;charset=utf-8',
759 'text/plain;charset=utf-8']
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500760
761 if ctype.lower() in JSON_ENC:
762 parse_resp = True
763 elif ctype.lower() in TXT_ENC:
764 parse_resp = False
765 else:
766 raise exceptions.UnexpectedContentType(str(resp.status),
767 resp=resp)
768
769 if resp.status == 401:
770 if parse_resp:
771 resp_body = self._parse_resp(resp_body)
772 raise exceptions.Unauthorized(resp_body, resp=resp)
773
774 if resp.status == 403:
775 if parse_resp:
776 resp_body = self._parse_resp(resp_body)
777 raise exceptions.Forbidden(resp_body, resp=resp)
778
779 if resp.status == 404:
780 if parse_resp:
781 resp_body = self._parse_resp(resp_body)
782 raise exceptions.NotFound(resp_body, resp=resp)
783
784 if resp.status == 400:
785 if parse_resp:
786 resp_body = self._parse_resp(resp_body)
787 raise exceptions.BadRequest(resp_body, resp=resp)
788
789 if resp.status == 410:
790 if parse_resp:
791 resp_body = self._parse_resp(resp_body)
792 raise exceptions.Gone(resp_body, resp=resp)
793
794 if resp.status == 409:
795 if parse_resp:
796 resp_body = self._parse_resp(resp_body)
797 raise exceptions.Conflict(resp_body, resp=resp)
798
Kevin Bentona82bc862017-02-13 01:16:13 -0800799 if resp.status == 412:
800 if parse_resp:
801 resp_body = self._parse_resp(resp_body)
802 raise exceptions.PreconditionFailed(resp_body, resp=resp)
803
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500804 if resp.status == 413:
805 if parse_resp:
806 resp_body = self._parse_resp(resp_body)
807 if self.is_absolute_limit(resp, resp_body):
808 raise exceptions.OverLimit(resp_body, resp=resp)
809 else:
810 raise exceptions.RateLimitExceeded(resp_body, resp=resp)
811
812 if resp.status == 415:
813 if parse_resp:
814 resp_body = self._parse_resp(resp_body)
815 raise exceptions.InvalidContentType(resp_body, resp=resp)
816
817 if resp.status == 422:
818 if parse_resp:
819 resp_body = self._parse_resp(resp_body)
820 raise exceptions.UnprocessableEntity(resp_body, resp=resp)
821
822 if resp.status in (500, 501):
823 message = resp_body
824 if parse_resp:
825 try:
826 resp_body = self._parse_resp(resp_body)
827 except ValueError:
828 # If response body is a non-json string message.
829 # Use resp_body as is and raise InvalidResponseBody
830 # exception.
831 raise exceptions.InvalidHTTPResponseBody(message)
832 else:
833 if isinstance(resp_body, dict):
834 # I'm seeing both computeFault
835 # and cloudServersFault come back.
836 # Will file a bug to fix, but leave as is for now.
837 if 'cloudServersFault' in resp_body:
838 message = resp_body['cloudServersFault']['message']
839 elif 'computeFault' in resp_body:
840 message = resp_body['computeFault']['message']
841 elif 'error' in resp_body:
842 message = resp_body['error']['message']
843 elif 'message' in resp_body:
844 message = resp_body['message']
845 else:
846 message = resp_body
847
848 if resp.status == 501:
849 raise exceptions.NotImplemented(resp_body, resp=resp,
850 message=message)
851 else:
852 raise exceptions.ServerFault(resp_body, resp=resp,
853 message=message)
854
855 if resp.status >= 400:
856 raise exceptions.UnexpectedResponseCode(str(resp.status),
857 resp=resp)
858
859 def is_absolute_limit(self, resp, resp_body):
860 if (not isinstance(resp_body, collections.Mapping) or
861 'retry-after' not in resp):
862 return True
Paul Glass119565a2016-04-06 11:41:42 -0500863 return 'exceed' in resp_body.get('message', 'blabla')
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500864
865 def wait_for_resource_deletion(self, id):
866 """Waits for a resource to be deleted
867
868 This method will loop over is_resource_deleted until either
869 is_resource_deleted returns True or the build timeout is reached. This
870 depends on is_resource_deleted being implemented
871
872 :param str id: The id of the resource to check
873 :raises TimeoutException: If the build_timeout has elapsed and the
874 resource still hasn't been deleted
875 """
876 start_time = int(time.time())
877 while True:
878 if self.is_resource_deleted(id):
879 return
880 if int(time.time()) - start_time >= self.build_timeout:
881 message = ('Failed to delete %(resource_type)s %(id)s within '
882 'the required time (%(timeout)s s).' %
883 {'resource_type': self.resource_type, 'id': id,
884 'timeout': self.build_timeout})
Jordan Pittier9e227c52016-02-09 14:35:18 +0100885 caller = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500886 if caller:
887 message = '(%s) %s' % (caller, message)
888 raise exceptions.TimeoutException(message)
889 time.sleep(self.build_interval)
890
891 def is_resource_deleted(self, id):
892 """Subclasses override with specific deletion detection."""
893 message = ('"%s" does not implement is_resource_deleted'
894 % self.__class__.__name__)
895 raise NotImplementedError(message)
896
897 @property
898 def resource_type(self):
899 """Returns the primary type of resource this client works with."""
900 return 'resource'
901
902 @classmethod
903 def validate_response(cls, schema, resp, body):
904 # Only check the response if the status code is a success code
905 # TODO(cyeoh): Eventually we should be able to verify that a failure
906 # code if it exists is something that we expect. This is explicitly
907 # declared in the V3 API and so we should be able to export this in
908 # the response schema. For now we'll ignore it.
909 if resp.status in HTTP_SUCCESS + HTTP_REDIRECTION:
910 cls.expected_success(schema['status_code'], resp.status)
911
912 # Check the body of a response
913 body_schema = schema.get('response_body')
914 if body_schema:
915 try:
916 jsonschema.validate(body, body_schema,
917 cls=JSONSCHEMA_VALIDATOR,
918 format_checker=FORMAT_CHECKER)
919 except jsonschema.ValidationError as ex:
guo yunxiana3f55282016-08-10 14:35:16 +0800920 msg = ("HTTP response body is invalid (%s)" % ex)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500921 raise exceptions.InvalidHTTPResponseBody(msg)
922 else:
923 if body:
guo yunxiana3f55282016-08-10 14:35:16 +0800924 msg = ("HTTP response body should not exist (%s)" % body)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500925 raise exceptions.InvalidHTTPResponseBody(msg)
926
927 # Check the header of a response
928 header_schema = schema.get('response_header')
929 if header_schema:
930 try:
931 jsonschema.validate(resp, header_schema,
932 cls=JSONSCHEMA_VALIDATOR,
933 format_checker=FORMAT_CHECKER)
934 except jsonschema.ValidationError as ex:
guo yunxiana3f55282016-08-10 14:35:16 +0800935 msg = ("HTTP response header is invalid (%s)" % ex)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500936 raise exceptions.InvalidHTTPResponseHeader(msg)
937
Ken'ichi Ohmichida26b162017-03-03 15:53:46 -0800938 def _get_base_version_url(self):
939 # TODO(oomichi): This method can be used for auth's replace_version().
940 # So it is nice to have common logic for the maintenance.
941 endpoint = self.base_url
942 url = urllib.parse.urlsplit(endpoint)
943 new_path = re.split(r'(^|/)+v\d+(\.\d+)?', url.path)[0]
944 url = list(url)
945 url[2] = new_path + '/'
946 return urllib.parse.urlunsplit(url)
947
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500948
949class ResponseBody(dict):
950 """Class that wraps an http response and dict body into a single value.
951
952 Callers that receive this object will normally use it as a dict but
953 can extract the response if needed.
954 """
955
956 def __init__(self, response, body=None):
957 body_data = body or {}
958 self.update(body_data)
959 self.response = response
960
961 def __str__(self):
962 body = super(ResponseBody, self).__str__()
963 return "response: %s\nBody: %s" % (self.response, body)
964
965
966class ResponseBodyData(object):
967 """Class that wraps an http response and string data into a single value.
968
969 """
970
971 def __init__(self, response, data):
972 self.response = response
973 self.data = data
974
975 def __str__(self):
976 return "response: %s\nBody: %s" % (self.response, self.data)
977
978
979class ResponseBodyList(list):
980 """Class that wraps an http response and list body into a single value.
981
982 Callers that receive this object will normally use it as a list but
983 can extract the response if needed.
984 """
985
986 def __init__(self, response, body=None):
987 body_data = body or []
988 self.extend(body_data)
989 self.response = response
990
991 def __str__(self):
992 body = super(ResponseBodyList, self).__str__()
993 return "response: %s\nBody: %s" % (self.response, body)