blob: 431a0a01e50f73d171992ade376fc94aa6d8a03e [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
Ilya Shakhat1291bb42017-11-29 18:08:16 +010030from tempest.lib.common import profiler
Jordan Pittier9e227c52016-02-09 14:35:18 +010031from tempest.lib.common.utils import test_utils
Matthew Treinish9e26ca82016-02-23 11:43:20 -050032from tempest.lib import exceptions
33
34# redrive rate limited calls at most twice
35MAX_RECURSION_DEPTH = 2
36
37# All the successful HTTP status codes from RFC 7231 & 4918
38HTTP_SUCCESS = (200, 201, 202, 203, 204, 205, 206, 207)
39
40# All the redirection HTTP status codes from RFC 7231 & 4918
41HTTP_REDIRECTION = (300, 301, 302, 303, 304, 305, 306, 307)
42
43# JSON Schema validator and format checker used for JSON Schema validation
ghanshyamf9ded352016-04-12 17:03:01 +090044JSONSCHEMA_VALIDATOR = jsonschema_validator.JSONSCHEMA_VALIDATOR
45FORMAT_CHECKER = jsonschema_validator.FORMAT_CHECKER
Matthew Treinish9e26ca82016-02-23 11:43:20 -050046
47
48class RestClient(object):
49 """Unified OpenStack RestClient class
50
51 This class is used for building openstack api clients on top of. It is
52 intended to provide a base layer for wrapping outgoing http requests in
53 keystone auth as well as providing response code checking and error
54 handling.
55
56 :param auth_provider: an auth provider object used to wrap requests in auth
57 :param str service: The service name to use for the catalog lookup
58 :param str region: The region to use for the catalog lookup
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -060059 :param str name: The endpoint name to use for the catalog lookup; this
60 returns only if the service exists
Matthew Treinish9e26ca82016-02-23 11:43:20 -050061 :param str endpoint_type: The endpoint type to use for the catalog lookup
62 :param int build_interval: Time in seconds between to status checks in
63 wait loops
64 :param int build_timeout: Timeout in seconds to wait for a wait operation.
65 :param bool disable_ssl_certificate_validation: Set to true to disable ssl
66 certificate validation
67 :param str ca_certs: File containing the CA Bundle to use in verifying a
68 TLS server cert
guo yunxian6f24cc42016-07-29 20:03:41 +080069 :param str trace_requests: Regex to use for specifying logging the entirety
Matthew Treinish9e26ca82016-02-23 11:43:20 -050070 of the request and response payload
zhufl071e94c2016-07-12 10:26:34 +080071 :param str http_timeout: Timeout in seconds to wait for the http request to
72 return
Matthew Treinish74514402016-09-01 11:44:57 -040073 :param str proxy_url: http proxy url to use.
Jens Harbott3ffa54e2018-07-04 11:59:49 +000074 :param bool follow_redirects: Set to false to stop following redirects.
Matthew Treinish9e26ca82016-02-23 11:43:20 -050075 """
Matthew Treinish9e26ca82016-02-23 11:43:20 -050076
77 # The version of the API this client implements
78 api_version = None
79
80 LOG = logging.getLogger(__name__)
81
82 def __init__(self, auth_provider, service, region,
83 endpoint_type='publicURL',
84 build_interval=1, build_timeout=60,
85 disable_ssl_certificate_validation=False, ca_certs=None,
Matthew Treinish74514402016-09-01 11:44:57 -040086 trace_requests='', name=None, http_timeout=None,
Jens Harbott3ffa54e2018-07-04 11:59:49 +000087 proxy_url=None, follow_redirects=True):
Matthew Treinish9e26ca82016-02-23 11:43:20 -050088 self.auth_provider = auth_provider
89 self.service = service
90 self.region = region
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -060091 self.name = name
Matthew Treinish9e26ca82016-02-23 11:43:20 -050092 self.endpoint_type = endpoint_type
93 self.build_interval = build_interval
94 self.build_timeout = build_timeout
95 self.trace_requests = trace_requests
96
97 self._skip_path = False
98 self.general_header_lc = set(('cache-control', 'connection',
99 'date', 'pragma', 'trailer',
100 'transfer-encoding', 'via',
101 'warning'))
102 self.response_header_lc = set(('accept-ranges', 'age', 'etag',
103 'location', 'proxy-authenticate',
104 'retry-after', 'server',
105 'vary', 'www-authenticate'))
106 dscv = disable_ssl_certificate_validation
Matthew Treinish74514402016-09-01 11:44:57 -0400107
108 if proxy_url:
109 self.http_obj = http.ClosingProxyHttp(
110 proxy_url,
111 disable_ssl_certificate_validation=dscv, ca_certs=ca_certs,
Jens Harbott3ffa54e2018-07-04 11:59:49 +0000112 timeout=http_timeout, follow_redirects=follow_redirects)
Matthew Treinish74514402016-09-01 11:44:57 -0400113 else:
114 self.http_obj = http.ClosingHttp(
115 disable_ssl_certificate_validation=dscv, ca_certs=ca_certs,
Jens Harbott3ffa54e2018-07-04 11:59:49 +0000116 timeout=http_timeout, follow_redirects=follow_redirects)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500117
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500118 def get_headers(self, accept_type=None, send_type=None):
119 """Return the default headers which will be used with outgoing requests
120
121 :param str accept_type: The media type to use for the Accept header, if
122 one isn't provided the object var TYPE will be
123 used
124 :param str send_type: The media-type to use for the Content-Type
125 header, if one isn't provided the object var
126 TYPE will be used
127 :rtype: dict
128 :return: The dictionary of headers which can be used in the headers
129 dict for outgoing request
130 """
131 if accept_type is None:
Masayuki Igawa189b92f2017-04-24 18:57:17 +0900132 accept_type = 'json'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500133 if send_type is None:
Masayuki Igawa189b92f2017-04-24 18:57:17 +0900134 send_type = 'json'
Ilya Shakhat1291bb42017-11-29 18:08:16 +0100135 headers = {'Content-Type': 'application/%s' % send_type,
136 'Accept': 'application/%s' % accept_type}
137 headers.update(profiler.serialize_as_http_headers())
138 return headers
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500139
140 def __str__(self):
141 STRING_LIMIT = 80
142 str_format = ("service:%s, base_url:%s, "
143 "filters: %s, build_interval:%s, build_timeout:%s"
144 "\ntoken:%s..., \nheaders:%s...")
145 return str_format % (self.service, self.base_url,
146 self.filters, self.build_interval,
147 self.build_timeout,
148 str(self.token)[0:STRING_LIMIT],
149 str(self.get_headers())[0:STRING_LIMIT])
150
151 @property
152 def user(self):
153 """The username used for requests
154
155 :rtype: string
156 :return: The username being used for requests
157 """
158
159 return self.auth_provider.credentials.username
160
161 @property
162 def user_id(self):
163 """The user_id used for requests
164
165 :rtype: string
166 :return: The user id being used for requests
167 """
168 return self.auth_provider.credentials.user_id
169
170 @property
171 def tenant_name(self):
172 """The tenant/project being used for requests
173
174 :rtype: string
175 :return: The tenant/project name being used for requests
176 """
177 return self.auth_provider.credentials.tenant_name
178
179 @property
180 def tenant_id(self):
181 """The tenant/project id being used for requests
182
183 :rtype: string
184 :return: The tenant/project id being used for requests
185 """
186 return self.auth_provider.credentials.tenant_id
187
188 @property
189 def password(self):
190 """The password being used for requests
191
192 :rtype: string
193 :return: The password being used for requests
194 """
195 return self.auth_provider.credentials.password
196
197 @property
198 def base_url(self):
199 return self.auth_provider.base_url(filters=self.filters)
200
201 @property
202 def token(self):
203 return self.auth_provider.get_token()
204
205 @property
206 def filters(self):
207 _filters = dict(
208 service=self.service,
209 endpoint_type=self.endpoint_type,
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -0600210 region=self.region,
211 name=self.name
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500212 )
213 if self.api_version is not None:
214 _filters['api_version'] = self.api_version
215 if self._skip_path:
216 _filters['skip_path'] = self._skip_path
217 return _filters
218
219 def skip_path(self):
220 """When set, ignore the path part of the base URL from the catalog"""
221 self._skip_path = True
222
223 def reset_path(self):
224 """When reset, use the base URL from the catalog as-is"""
225 self._skip_path = False
226
227 @classmethod
228 def expected_success(cls, expected_code, read_code):
229 """Check expected success response code against the http response
230
231 :param int expected_code: The response code that is expected.
232 Optionally a list of integers can be used
233 to specify multiple valid success codes
234 :param int read_code: The response code which was returned in the
235 response
236 :raises AssertionError: if the expected_code isn't a valid http success
237 response code
238 :raises exceptions.InvalidHttpSuccessCode: if the read code isn't an
239 expected http success code
240 """
ghanshyamc3074202016-04-18 15:20:45 +0900241 if not isinstance(read_code, int):
242 raise TypeError("'read_code' must be an int instead of (%s)"
243 % type(read_code))
244
Hanxi2f977db2016-09-01 17:31:28 +0800245 assert_msg = ("This function only allowed to use for HTTP status "
246 "codes which explicitly defined in the RFC 7231 & 4918. "
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500247 "{0} is not a defined Success Code!"
248 ).format(expected_code)
249 if isinstance(expected_code, list):
250 for code in expected_code:
251 assert code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg
252 else:
253 assert expected_code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg
254
255 # NOTE(afazekas): the http status code above 400 is processed by
256 # the _error_checker method
257 if read_code < 400:
zhufl4d2f5152017-01-17 11:16:12 +0800258 pattern = ("Unexpected http success status code {0}, "
259 "The expected status code is {1}")
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500260 if ((not isinstance(expected_code, list) and
261 (read_code != expected_code)) or
262 (isinstance(expected_code, list) and
263 (read_code not in expected_code))):
264 details = pattern.format(read_code, expected_code)
265 raise exceptions.InvalidHttpSuccessCode(details)
266
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200267 def post(self, url, body, headers=None, extra_headers=False,
268 chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500269 """Send a HTTP POST request using keystone auth
270
271 :param str url: the relative url to send the post request to
272 :param dict body: the request body
273 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300274 :param bool extra_headers: Boolean value than indicates if the headers
275 returned by the get_headers() method are to
276 be used but additional headers are needed in
277 the request pass them in as a dict.
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200278 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500279 :return: a tuple with the first entry containing the response headers
280 and the second the response body
281 :rtype: tuple
282 """
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200283 return self.request('POST', url, extra_headers, headers, body, chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500284
285 def get(self, url, headers=None, extra_headers=False):
286 """Send a HTTP GET request using keystone service catalog and auth
287
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530288 :param str url: the relative url to send the get request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500289 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300290 :param bool extra_headers: Boolean value than indicates if the headers
291 returned by the get_headers() method are to
292 be used but additional headers are needed in
293 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500294 :return: a tuple with the first entry containing the response headers
295 and the second the response body
296 :rtype: tuple
297 """
298 return self.request('GET', url, extra_headers, headers)
299
300 def delete(self, url, headers=None, body=None, extra_headers=False):
301 """Send a HTTP DELETE request using keystone service catalog and auth
302
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530303 :param str url: the relative url to send the delete request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500304 :param dict headers: The headers to use for the request
305 :param dict body: the request body
vsaienko9eb846b2016-04-09 00:35:47 +0300306 :param bool extra_headers: Boolean value than indicates if the headers
307 returned by the get_headers() method are to
308 be used but additional headers are needed in
309 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500310 :return: a tuple with the first entry containing the response headers
311 and the second the response body
312 :rtype: tuple
313 """
314 return self.request('DELETE', url, extra_headers, headers, body)
315
316 def patch(self, url, body, headers=None, extra_headers=False):
317 """Send a HTTP PATCH request using keystone service catalog and auth
318
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530319 :param str url: the relative url to send the patch request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500320 :param dict body: the request body
321 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300322 :param bool extra_headers: Boolean value than indicates if the headers
323 returned by the get_headers() method are to
324 be used but additional headers are needed in
325 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500326 :return: a tuple with the first entry containing the response headers
327 and the second the response body
328 :rtype: tuple
329 """
330 return self.request('PATCH', url, extra_headers, headers, body)
331
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200332 def put(self, url, body, headers=None, extra_headers=False, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500333 """Send a HTTP PUT request using keystone service catalog and auth
334
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530335 :param str url: the relative url to send the put request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500336 :param dict body: the request body
337 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300338 :param bool extra_headers: Boolean value than indicates if the headers
339 returned by the get_headers() method are to
340 be used but additional headers are needed in
341 the request pass them in as a dict.
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200342 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500343 :return: a tuple with the first entry containing the response headers
344 and the second the response body
345 :rtype: tuple
346 """
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200347 return self.request('PUT', url, extra_headers, headers, body, chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500348
349 def head(self, url, headers=None, extra_headers=False):
350 """Send a HTTP HEAD request using keystone service catalog and auth
351
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530352 :param str url: the relative url to send the head request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500353 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300354 :param bool extra_headers: Boolean value than indicates if the headers
355 returned by the get_headers() method are to
356 be used but additional headers are needed in
357 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500358 :return: a tuple with the first entry containing the response headers
359 and the second the response body
360 :rtype: tuple
361 """
362 return self.request('HEAD', url, extra_headers, headers)
363
364 def copy(self, url, headers=None, extra_headers=False):
365 """Send a HTTP COPY request using keystone service catalog and auth
366
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530367 :param str url: the relative url to send the copy request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500368 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300369 :param bool extra_headers: Boolean value than indicates if the headers
370 returned by the get_headers() method are to
371 be used but additional headers are needed in
372 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500373 :return: a tuple with the first entry containing the response headers
374 and the second the response body
375 :rtype: tuple
376 """
377 return self.request('COPY', url, extra_headers, headers)
378
379 def get_versions(self):
sunqingliang699690f62018-11-09 15:03:17 +0800380 """Get the versions on an endpoint from the keystone catalog
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500381
382 This method will make a GET request on the baseurl from the keystone
383 catalog to return a list of API versions. It is expected that a GET
384 on the endpoint in the catalog will return a list of supported API
385 versions.
386
junboli872ca872017-07-21 13:24:38 +0800387 :return: tuple with response headers and list of version numbers
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500388 :rtype: tuple
389 """
390 resp, body = self.get('')
391 body = self._parse_resp(body)
392 versions = map(lambda x: x['id'], body)
393 return resp, versions
394
395 def _get_request_id(self, resp):
396 for i in ('x-openstack-request-id', 'x-compute-request-id'):
397 if i in resp:
398 return resp[i]
399 return ""
400
401 def _safe_body(self, body, maxlen=4096):
402 # convert a structure into a string safely
403 try:
404 text = six.text_type(body)
405 except UnicodeDecodeError:
406 # if this isn't actually text, return marker that
407 return "<BinaryData: removed>"
408 if len(text) > maxlen:
409 return text[:maxlen]
410 else:
411 return text
412
guo yunxian9f749f92016-08-25 10:55:04 +0800413 def _log_request_start(self, method, req_url):
Jordan Pittier9e227c52016-02-09 14:35:18 +0100414 caller_name = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500415 if self.trace_requests and re.search(self.trace_requests, caller_name):
Jordan Pittier525ec712016-12-07 17:51:26 +0100416 self.LOG.debug('Starting Request (%s): %s %s', caller_name,
417 method, req_url)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500418
guo yunxian9f749f92016-08-25 10:55:04 +0800419 def _log_request_full(self, resp, req_headers=None, req_body=None,
420 resp_body=None, extra=None):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500421 if 'X-Auth-Token' in req_headers:
422 req_headers['X-Auth-Token'] = '<omitted>'
Ken'ichi Ohmichi2902a7b2018-07-14 02:31:03 +0000423 if 'X-Subject-Token' in req_headers:
424 req_headers['X-Subject-Token'] = '<omitted>'
Andrea Frittoli (andreaf)a1edb2d2016-05-10 16:09:59 +0100425 # A shallow copy is sufficient
426 resp_log = resp.copy()
427 if 'x-subject-token' in resp_log:
428 resp_log['x-subject-token'] = '<omitted>'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500429 log_fmt = """Request - Headers: %s
430 Body: %s
431 Response - Headers: %s
432 Body: %s"""
433
434 self.LOG.debug(
Jordan Pittier525ec712016-12-07 17:51:26 +0100435 log_fmt,
436 str(req_headers),
437 self._safe_body(req_body),
438 str(resp_log),
439 self._safe_body(resp_body),
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500440 extra=extra)
441
442 def _log_request(self, method, req_url, resp,
443 secs="", req_headers=None,
444 req_body=None, resp_body=None):
445 if req_headers is None:
446 req_headers = {}
447 # if we have the request id, put it in the right part of the log
448 extra = dict(request_id=self._get_request_id(resp))
449 # NOTE(sdague): while we still have 6 callers to this function
450 # we're going to just provide work around on who is actually
451 # providing timings by gracefully adding no content if they don't.
452 # Once we're down to 1 caller, clean this up.
Jordan Pittier9e227c52016-02-09 14:35:18 +0100453 caller_name = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500454 if secs:
455 secs = " %.3fs" % secs
456 self.LOG.info(
Jordan Pittier525ec712016-12-07 17:51:26 +0100457 'Request (%s): %s %s %s%s',
458 caller_name,
459 resp['status'],
460 method,
461 req_url,
462 secs,
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500463 extra=extra)
464
465 # Also look everything at DEBUG if you want to filter this
466 # out, don't run at debug.
Anusha Raminenif3eb9472017-01-13 08:54:01 +0530467 if self.LOG.isEnabledFor(logging.DEBUG):
guo yunxian9f749f92016-08-25 10:55:04 +0800468 self._log_request_full(resp, req_headers, req_body,
469 resp_body, extra)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500470
471 def _parse_resp(self, body):
472 try:
473 body = json.loads(body)
474 except ValueError:
475 return body
476
477 # We assume, that if the first value of the deserialized body's
478 # item set is a dict or a list, that we just return the first value
479 # of deserialized body.
480 # Essentially "cutting out" the first placeholder element in a body
481 # that looks like this:
482 #
483 # {
484 # "users": [
485 # ...
486 # ]
487 # }
488 try:
489 # Ensure there are not more than one top-level keys
490 # NOTE(freerunner): Ensure, that JSON is not nullable to
491 # to prevent StopIteration Exception
Ken'ichi Ohmichi69a8edc2017-04-28 11:41:20 -0700492 if not hasattr(body, "keys") or len(body.keys()) != 1:
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500493 return body
494 # Just return the "wrapped" element
495 first_key, first_item = six.next(six.iteritems(body))
496 if isinstance(first_item, (dict, list)):
497 return first_item
498 except (ValueError, IndexError):
499 pass
500 return body
501
502 def response_checker(self, method, resp, resp_body):
503 """A sanity check on the response from a HTTP request
504
505 This method does a sanity check on whether the response from an HTTP
506 request conforms the HTTP RFC.
507
508 :param str method: The HTTP verb of the request associated with the
509 response being passed in.
510 :param resp: The response headers
511 :param resp_body: The body of the response
512 :raises ResponseWithNonEmptyBody: If the response with the status code
513 is not supposed to have a body
514 :raises ResponseWithEntity: If the response code is 205 but has an
515 entity
516 """
517 if (resp.status in set((204, 205, 304)) or resp.status < 200 or
518 method.upper() == 'HEAD') and resp_body:
519 raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
520 # NOTE(afazekas):
521 # If the HTTP Status Code is 205
522 # 'The response MUST NOT include an entity.'
523 # A HTTP entity has an entity-body and an 'entity-header'.
524 # In the HTTP response specification (Section 6) the 'entity-header'
525 # 'generic-header' and 'response-header' are in OR relation.
526 # All headers not in the above two group are considered as entity
527 # header in every interpretation.
528
529 if (resp.status == 205 and
530 0 != len(set(resp.keys()) - set(('status',)) -
531 self.response_header_lc - self.general_header_lc)):
Matt Riedemann91d92422019-01-29 16:19:49 -0500532 raise exceptions.ResponseWithEntity()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500533 # NOTE(afazekas)
534 # Now the swift sometimes (delete not empty container)
535 # returns with non json error response, we can create new rest class
536 # for swift.
537 # Usually RFC2616 says error responses SHOULD contain an explanation.
538 # The warning is normal for SHOULD/SHOULD NOT case
539
540 # Likely it will cause an error
541 if method != 'HEAD' and not resp_body and resp.status >= 400:
542 self.LOG.warning("status >= 400 response with empty body")
543
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200544 def _request(self, method, url, headers=None, body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500545 """A simple HTTP request interface."""
546 # Authenticate the request with the auth provider
547 req_url, req_headers, req_body = self.auth_provider.auth_request(
548 method, url, headers, body, self.filters)
549
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500550 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 # Verify HTTP response codes
555 self.response_checker(method, resp, resp_body)
556
557 return resp, resp_body
558
Ghanshyam Mann47a41992019-10-02 16:56:26 +0000559 def raw_request(self, url, method, headers=None, body=None, chunked=False,
560 log_req_body=None):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500561 """Send a raw HTTP request without the keystone catalog or auth
562
563 This method sends a HTTP request in the same manner as the request()
564 method, however it does so without using keystone auth or the catalog
565 to determine the base url. Additionally no response handling is done
566 the results from the request are just returned.
567
568 :param str url: Full url to send the request
569 :param str method: The HTTP verb to use for the request
zhuflcf35f652018-08-17 10:13:43 +0800570 :param dict headers: Headers to use for the request. If none are
571 specified, then the headers returned from the
572 get_headers() method are used. If the request
573 explicitly requires no headers use an empty dict.
Anh Trand44a8be2016-03-25 09:49:14 +0700574 :param str body: Body to send with the request
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200575 :param bool chunked: sends the body with chunked encoding
Ghanshyam Mann47a41992019-10-02 16:56:26 +0000576 :param str log_req_body: Whether to log the request body or not.
577 It is default to None which means request
578 body is safe to log otherwise pass any string
579 you want to log in place of request body.
580 For example: '<omitted>'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500581 :rtype: tuple
582 :return: a tuple with the first entry containing the response headers
583 and the second the response body
584 """
585 if headers is None:
586 headers = self.get_headers()
Ghanshyam Manncb3cf032019-09-26 00:02:54 +0000587 # Do the actual request, and time it
588 start = time.time()
589 self._log_request_start(method, url)
590 resp, resp_body = self.http_obj.request(
591 url, method, headers=headers,
592 body=body, chunked=chunked)
593 end = time.time()
Ghanshyam Mann47a41992019-10-02 16:56:26 +0000594 req_body = body if log_req_body is None else log_req_body
Ghanshyam Manncb3cf032019-09-26 00:02:54 +0000595 self._log_request(method, url, resp, secs=(end - start),
Ghanshyam Mann47a41992019-10-02 16:56:26 +0000596 req_headers=headers, req_body=req_body,
Ghanshyam Manncb3cf032019-09-26 00:02:54 +0000597 resp_body=resp_body)
598 return resp, resp_body
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500599
600 def request(self, method, url, extra_headers=False, headers=None,
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200601 body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500602 """Send a HTTP request with keystone auth and using the catalog
603
604 This method will send an HTTP request using keystone auth in the
605 headers and the catalog to determine the endpoint to use for the
606 baseurl to send the request to. Additionally
607
608 When a response is received it will check it to see if an error
609 response was received. If it was an exception will be raised to enable
610 it to be handled quickly.
611
612 This method will also handle rate-limiting, if a 413 response code is
613 received it will retry the request after waiting the 'retry-after'
614 duration from the header.
615
616 :param str method: The HTTP verb to use for the request
617 :param str url: Relative url to send the request to
vsaienko9eb846b2016-04-09 00:35:47 +0300618 :param bool extra_headers: Boolean value than indicates if the headers
619 returned by the get_headers() method are to
620 be used but additional headers are needed in
621 the request pass them in as a dict.
zhuflcf35f652018-08-17 10:13:43 +0800622 :param dict headers: Headers to use for the request. If none are
623 specified, then the headers returned from the
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500624 get_headers() method are used. If the request
625 explicitly requires no headers use an empty dict.
Anh Trand44a8be2016-03-25 09:49:14 +0700626 :param str body: Body to send with the request
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200627 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500628 :rtype: tuple
629 :return: a tuple with the first entry containing the response headers
630 and the second the response body
631 :raises UnexpectedContentType: If the content-type of the response
632 isn't an expect type
633 :raises Unauthorized: If a 401 response code is received
634 :raises Forbidden: If a 403 response code is received
635 :raises NotFound: If a 404 response code is received
636 :raises BadRequest: If a 400 response code is received
637 :raises Gone: If a 410 response code is received
638 :raises Conflict: If a 409 response code is received
Kevin Bentona82bc862017-02-13 01:16:13 -0800639 :raises PreconditionFailed: If a 412 response code is received
zhuflcf35f652018-08-17 10:13:43 +0800640 :raises OverLimit: If a 413 response code is received and retry-after
641 is not in the response body or its retry operation
642 exceeds the limits defined by the server
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500643 :raises RateLimitExceeded: If a 413 response code is received and
zhuflcf35f652018-08-17 10:13:43 +0800644 retry-after is in the response body and
645 its retry operation does not exceeds the
646 limits defined by the server
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500647 :raises InvalidContentType: If a 415 response code is received
648 :raises UnprocessableEntity: If a 422 response code is received
649 :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
650 and couldn't be parsed
651 :raises NotImplemented: If a 501 response code is received
652 :raises ServerFault: If a 500 response code is received
653 :raises UnexpectedResponseCode: If a response code above 400 is
654 received and it doesn't fall into any
655 of the handled checks
656 """
657 # if extra_headers is True
658 # default headers would be added to headers
659 retry = 0
660
661 if headers is None:
662 # NOTE(vponomaryov): if some client do not need headers,
663 # it should explicitly pass empty dict
664 headers = self.get_headers()
665 elif extra_headers:
666 try:
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500667 headers.update(self.get_headers())
668 except (ValueError, TypeError):
669 headers = self.get_headers()
670
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200671 resp, resp_body = self._request(method, url, headers=headers,
672 body=body, chunked=chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500673
674 while (resp.status == 413 and
675 'retry-after' in resp and
676 not self.is_absolute_limit(
677 resp, self._parse_resp(resp_body)) and
678 retry < MAX_RECURSION_DEPTH):
679 retry += 1
Paul Glass119565a2016-04-06 11:41:42 -0500680 delay = self._get_retry_after_delay(resp)
681 self.LOG.debug(
682 "Sleeping %s seconds based on retry-after header", delay
683 )
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500684 time.sleep(delay)
685 resp, resp_body = self._request(method, url,
686 headers=headers, body=body)
Ken'ichi Ohmichi17051e82016-10-20 11:46:06 -0700687 self._error_checker(resp, resp_body)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500688 return resp, resp_body
689
Paul Glass119565a2016-04-06 11:41:42 -0500690 def _get_retry_after_delay(self, resp):
691 """Extract the delay from the retry-after header.
692
693 This supports both integer and HTTP date formatted retry-after headers
694 per RFC 2616.
695
696 :param resp: The response containing the retry-after headers
697 :rtype: int
698 :return: The delay in seconds, clamped to be at least 1 second
699 :raises ValueError: On failing to parse the delay
700 """
701 delay = None
702 try:
703 delay = int(resp['retry-after'])
704 except (ValueError, KeyError):
705 pass
706
707 try:
708 retry_timestamp = self._parse_http_date(resp['retry-after'])
709 date_timestamp = self._parse_http_date(resp['date'])
710 delay = int(retry_timestamp - date_timestamp)
711 except (ValueError, OverflowError, KeyError):
712 pass
713
714 if delay is None:
715 raise ValueError(
716 "Failed to parse retry-after header %r as either int or "
717 "HTTP-date." % resp.get('retry-after')
718 )
719
720 # Retry-after headers do not have sub-second precision. Clients may
721 # receive a delay of 0. After sleeping 0 seconds, we would (likely) hit
722 # another 413. To avoid this, always sleep at least 1 second.
723 return max(1, delay)
724
725 def _parse_http_date(self, val):
726 """Parse an HTTP date, like 'Fri, 31 Dec 1999 23:59:59 GMT'.
727
728 Return an epoch timestamp (float), as returned by time.mktime().
729 """
730 parts = email.utils.parsedate(val)
731 if not parts:
732 raise ValueError("Failed to parse date %s" % val)
733 return time.mktime(parts)
734
Ken'ichi Ohmichi17051e82016-10-20 11:46:06 -0700735 def _error_checker(self, resp, resp_body):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500736
737 # NOTE(mtreinish): Check for httplib response from glance_http. The
738 # object can't be used here because importing httplib breaks httplib2.
739 # If another object from a class not imported were passed here as
740 # resp this could possibly fail
741 if str(type(resp)) == "<type 'instance'>":
742 ctype = resp.getheader('content-type')
743 else:
744 try:
745 ctype = resp['content-type']
746 # NOTE(mtreinish): Keystone delete user responses doesn't have a
747 # content-type header. (They don't have a body) So just pretend it
748 # is set.
749 except KeyError:
750 ctype = 'application/json'
751
752 # It is not an error response
753 if resp.status < 400:
754 return
755
zhipenghd1db0c72017-02-21 04:40:07 -0500756 # NOTE(zhipengh): There is a purposefully duplicate of content-type
757 # with the only difference is with or without spaces, as specified
758 # in RFC7231.
759 JSON_ENC = ['application/json', 'application/json; charset=utf-8',
760 'application/json;charset=utf-8']
761
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500762 # NOTE(mtreinish): This is for compatibility with Glance and swift
763 # APIs. These are the return content types that Glance api v1
764 # (and occasionally swift) are using.
zhipenghd1db0c72017-02-21 04:40:07 -0500765 # NOTE(zhipengh): There is a purposefully duplicate of content-type
766 # with the only difference is with or without spaces, as specified
767 # in RFC7231.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500768 TXT_ENC = ['text/plain', 'text/html', 'text/html; charset=utf-8',
zhipenghd1db0c72017-02-21 04:40:07 -0500769 'text/plain; charset=utf-8', 'text/html;charset=utf-8',
770 'text/plain;charset=utf-8']
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500771
772 if ctype.lower() in JSON_ENC:
773 parse_resp = True
774 elif ctype.lower() in TXT_ENC:
775 parse_resp = False
776 else:
777 raise exceptions.UnexpectedContentType(str(resp.status),
778 resp=resp)
779
780 if resp.status == 401:
781 if parse_resp:
782 resp_body = self._parse_resp(resp_body)
783 raise exceptions.Unauthorized(resp_body, resp=resp)
784
785 if resp.status == 403:
786 if parse_resp:
787 resp_body = self._parse_resp(resp_body)
788 raise exceptions.Forbidden(resp_body, resp=resp)
789
790 if resp.status == 404:
791 if parse_resp:
792 resp_body = self._parse_resp(resp_body)
793 raise exceptions.NotFound(resp_body, resp=resp)
794
795 if resp.status == 400:
796 if parse_resp:
797 resp_body = self._parse_resp(resp_body)
798 raise exceptions.BadRequest(resp_body, resp=resp)
799
800 if resp.status == 410:
801 if parse_resp:
802 resp_body = self._parse_resp(resp_body)
803 raise exceptions.Gone(resp_body, resp=resp)
804
805 if resp.status == 409:
806 if parse_resp:
807 resp_body = self._parse_resp(resp_body)
808 raise exceptions.Conflict(resp_body, resp=resp)
809
Kevin Bentona82bc862017-02-13 01:16:13 -0800810 if resp.status == 412:
811 if parse_resp:
812 resp_body = self._parse_resp(resp_body)
813 raise exceptions.PreconditionFailed(resp_body, resp=resp)
814
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500815 if resp.status == 413:
816 if parse_resp:
817 resp_body = self._parse_resp(resp_body)
818 if self.is_absolute_limit(resp, resp_body):
819 raise exceptions.OverLimit(resp_body, resp=resp)
820 else:
821 raise exceptions.RateLimitExceeded(resp_body, resp=resp)
822
823 if resp.status == 415:
824 if parse_resp:
825 resp_body = self._parse_resp(resp_body)
826 raise exceptions.InvalidContentType(resp_body, resp=resp)
827
828 if resp.status == 422:
829 if parse_resp:
830 resp_body = self._parse_resp(resp_body)
831 raise exceptions.UnprocessableEntity(resp_body, resp=resp)
832
833 if resp.status in (500, 501):
834 message = resp_body
835 if parse_resp:
836 try:
837 resp_body = self._parse_resp(resp_body)
838 except ValueError:
839 # If response body is a non-json string message.
840 # Use resp_body as is and raise InvalidResponseBody
841 # exception.
842 raise exceptions.InvalidHTTPResponseBody(message)
843 else:
844 if isinstance(resp_body, dict):
845 # I'm seeing both computeFault
846 # and cloudServersFault come back.
847 # Will file a bug to fix, but leave as is for now.
848 if 'cloudServersFault' in resp_body:
849 message = resp_body['cloudServersFault']['message']
850 elif 'computeFault' in resp_body:
851 message = resp_body['computeFault']['message']
852 elif 'error' in resp_body:
853 message = resp_body['error']['message']
854 elif 'message' in resp_body:
855 message = resp_body['message']
856 else:
857 message = resp_body
858
859 if resp.status == 501:
860 raise exceptions.NotImplemented(resp_body, resp=resp,
861 message=message)
862 else:
863 raise exceptions.ServerFault(resp_body, resp=resp,
864 message=message)
865
866 if resp.status >= 400:
867 raise exceptions.UnexpectedResponseCode(str(resp.status),
868 resp=resp)
869
870 def is_absolute_limit(self, resp, resp_body):
871 if (not isinstance(resp_body, collections.Mapping) or
872 'retry-after' not in resp):
873 return True
Paul Glass119565a2016-04-06 11:41:42 -0500874 return 'exceed' in resp_body.get('message', 'blabla')
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500875
876 def wait_for_resource_deletion(self, id):
877 """Waits for a resource to be deleted
878
879 This method will loop over is_resource_deleted until either
880 is_resource_deleted returns True or the build timeout is reached. This
881 depends on is_resource_deleted being implemented
882
883 :param str id: The id of the resource to check
884 :raises TimeoutException: If the build_timeout has elapsed and the
885 resource still hasn't been deleted
886 """
887 start_time = int(time.time())
888 while True:
889 if self.is_resource_deleted(id):
890 return
891 if int(time.time()) - start_time >= self.build_timeout:
892 message = ('Failed to delete %(resource_type)s %(id)s within '
893 'the required time (%(timeout)s s).' %
894 {'resource_type': self.resource_type, 'id': id,
895 'timeout': self.build_timeout})
Jordan Pittier9e227c52016-02-09 14:35:18 +0100896 caller = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500897 if caller:
898 message = '(%s) %s' % (caller, message)
899 raise exceptions.TimeoutException(message)
900 time.sleep(self.build_interval)
901
902 def is_resource_deleted(self, id):
903 """Subclasses override with specific deletion detection."""
904 message = ('"%s" does not implement is_resource_deleted'
905 % self.__class__.__name__)
906 raise NotImplementedError(message)
907
908 @property
909 def resource_type(self):
910 """Returns the primary type of resource this client works with."""
911 return 'resource'
912
913 @classmethod
914 def validate_response(cls, schema, resp, body):
915 # Only check the response if the status code is a success code
916 # TODO(cyeoh): Eventually we should be able to verify that a failure
917 # code if it exists is something that we expect. This is explicitly
918 # declared in the V3 API and so we should be able to export this in
919 # the response schema. For now we'll ignore it.
920 if resp.status in HTTP_SUCCESS + HTTP_REDIRECTION:
921 cls.expected_success(schema['status_code'], resp.status)
922
923 # Check the body of a response
924 body_schema = schema.get('response_body')
925 if body_schema:
926 try:
927 jsonschema.validate(body, body_schema,
928 cls=JSONSCHEMA_VALIDATOR,
929 format_checker=FORMAT_CHECKER)
930 except jsonschema.ValidationError as ex:
guo yunxiana3f55282016-08-10 14:35:16 +0800931 msg = ("HTTP response body is invalid (%s)" % ex)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500932 raise exceptions.InvalidHTTPResponseBody(msg)
933 else:
934 if body:
guo yunxiana3f55282016-08-10 14:35:16 +0800935 msg = ("HTTP response body should not exist (%s)" % body)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500936 raise exceptions.InvalidHTTPResponseBody(msg)
937
938 # Check the header of a response
939 header_schema = schema.get('response_header')
940 if header_schema:
941 try:
942 jsonschema.validate(resp, header_schema,
943 cls=JSONSCHEMA_VALIDATOR,
944 format_checker=FORMAT_CHECKER)
945 except jsonschema.ValidationError as ex:
guo yunxiana3f55282016-08-10 14:35:16 +0800946 msg = ("HTTP response header is invalid (%s)" % ex)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500947 raise exceptions.InvalidHTTPResponseHeader(msg)
948
Ken'ichi Ohmichida26b162017-03-03 15:53:46 -0800949 def _get_base_version_url(self):
950 # TODO(oomichi): This method can be used for auth's replace_version().
951 # So it is nice to have common logic for the maintenance.
952 endpoint = self.base_url
953 url = urllib.parse.urlsplit(endpoint)
954 new_path = re.split(r'(^|/)+v\d+(\.\d+)?', url.path)[0]
955 url = list(url)
956 url[2] = new_path + '/'
957 return urllib.parse.urlunsplit(url)
958
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500959
960class ResponseBody(dict):
961 """Class that wraps an http response and dict body into a single value.
962
963 Callers that receive this object will normally use it as a dict but
964 can extract the response if needed.
965 """
966
967 def __init__(self, response, body=None):
968 body_data = body or {}
969 self.update(body_data)
970 self.response = response
971
972 def __str__(self):
973 body = super(ResponseBody, self).__str__()
974 return "response: %s\nBody: %s" % (self.response, body)
975
976
977class ResponseBodyData(object):
978 """Class that wraps an http response and string data into a single value.
979
980 """
981
982 def __init__(self, response, data):
983 self.response = response
984 self.data = data
985
986 def __str__(self):
987 return "response: %s\nBody: %s" % (self.response, self.data)
988
989
990class ResponseBodyList(list):
991 """Class that wraps an http response and list body into a single value.
992
993 Callers that receive this object will normally use it as a list but
994 can extract the response if needed.
995 """
996
997 def __init__(self, response, body=None):
998 body_data = body or []
999 self.extend(body_data)
1000 self.response = response
1001
1002 def __str__(self):
1003 body = super(ResponseBodyList, self).__str__()
1004 return "response: %s\nBody: %s" % (self.response, body)