blob: b47b511f6b127b6856bdbd80cda8b37b8ef1ff3b [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
Rodolfo Alonso Hernandezc1449d42020-02-15 13:24:28 +000024from oslo_log import versionutils
Matthew Treinish9e26ca82016-02-23 11:43:20 -050025from oslo_serialization import jsonutils as json
26import six
Ken'ichi Ohmichida26b162017-03-03 15:53:46 -080027from six.moves import urllib
Matthew Treinish9e26ca82016-02-23 11:43:20 -050028
29from tempest.lib.common import http
ghanshyamf9ded352016-04-12 17:03:01 +090030from tempest.lib.common import jsonschema_validator
Ilya Shakhat1291bb42017-11-29 18:08:16 +010031from tempest.lib.common import profiler
Jordan Pittier9e227c52016-02-09 14:35:18 +010032from tempest.lib.common.utils import test_utils
Matthew Treinish9e26ca82016-02-23 11:43:20 -050033from tempest.lib import exceptions
34
35# redrive rate limited calls at most twice
36MAX_RECURSION_DEPTH = 2
37
38# All the successful HTTP status codes from RFC 7231 & 4918
39HTTP_SUCCESS = (200, 201, 202, 203, 204, 205, 206, 207)
40
41# All the redirection HTTP status codes from RFC 7231 & 4918
42HTTP_REDIRECTION = (300, 301, 302, 303, 304, 305, 306, 307)
43
44# JSON Schema validator and format checker used for JSON Schema validation
ghanshyamf9ded352016-04-12 17:03:01 +090045JSONSCHEMA_VALIDATOR = jsonschema_validator.JSONSCHEMA_VALIDATOR
46FORMAT_CHECKER = jsonschema_validator.FORMAT_CHECKER
Matthew Treinish9e26ca82016-02-23 11:43:20 -050047
48
49class RestClient(object):
50 """Unified OpenStack RestClient class
51
52 This class is used for building openstack api clients on top of. It is
53 intended to provide a base layer for wrapping outgoing http requests in
54 keystone auth as well as providing response code checking and error
55 handling.
56
57 :param auth_provider: an auth provider object used to wrap requests in auth
58 :param str service: The service name to use for the catalog lookup
59 :param str region: The region to use for the catalog lookup
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -060060 :param str name: The endpoint name to use for the catalog lookup; this
61 returns only if the service exists
Matthew Treinish9e26ca82016-02-23 11:43:20 -050062 :param str endpoint_type: The endpoint type to use for the catalog lookup
63 :param int build_interval: Time in seconds between to status checks in
64 wait loops
65 :param int build_timeout: Timeout in seconds to wait for a wait operation.
66 :param bool disable_ssl_certificate_validation: Set to true to disable ssl
67 certificate validation
68 :param str ca_certs: File containing the CA Bundle to use in verifying a
69 TLS server cert
guo yunxian6f24cc42016-07-29 20:03:41 +080070 :param str trace_requests: Regex to use for specifying logging the entirety
Matthew Treinish9e26ca82016-02-23 11:43:20 -050071 of the request and response payload
zhufl071e94c2016-07-12 10:26:34 +080072 :param str http_timeout: Timeout in seconds to wait for the http request to
73 return
Matthew Treinish74514402016-09-01 11:44:57 -040074 :param str proxy_url: http proxy url to use.
Jens Harbott3ffa54e2018-07-04 11:59:49 +000075 :param bool follow_redirects: Set to false to stop following redirects.
Matthew Treinish9e26ca82016-02-23 11:43:20 -050076 """
Matthew Treinish9e26ca82016-02-23 11:43:20 -050077
78 # The version of the API this client implements
79 api_version = None
80
81 LOG = logging.getLogger(__name__)
82
83 def __init__(self, auth_provider, service, region,
84 endpoint_type='publicURL',
85 build_interval=1, build_timeout=60,
86 disable_ssl_certificate_validation=False, ca_certs=None,
Matthew Treinish74514402016-09-01 11:44:57 -040087 trace_requests='', name=None, http_timeout=None,
Jens Harbott3ffa54e2018-07-04 11:59:49 +000088 proxy_url=None, follow_redirects=True):
Matthew Treinish9e26ca82016-02-23 11:43:20 -050089 self.auth_provider = auth_provider
90 self.service = service
91 self.region = region
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -060092 self.name = name
Matthew Treinish9e26ca82016-02-23 11:43:20 -050093 self.endpoint_type = endpoint_type
94 self.build_interval = build_interval
95 self.build_timeout = build_timeout
96 self.trace_requests = trace_requests
97
98 self._skip_path = False
99 self.general_header_lc = set(('cache-control', 'connection',
100 'date', 'pragma', 'trailer',
101 'transfer-encoding', 'via',
102 'warning'))
103 self.response_header_lc = set(('accept-ranges', 'age', 'etag',
104 'location', 'proxy-authenticate',
105 'retry-after', 'server',
106 'vary', 'www-authenticate'))
107 dscv = disable_ssl_certificate_validation
Matthew Treinish74514402016-09-01 11:44:57 -0400108
109 if proxy_url:
110 self.http_obj = http.ClosingProxyHttp(
111 proxy_url,
112 disable_ssl_certificate_validation=dscv, ca_certs=ca_certs,
Jens Harbott3ffa54e2018-07-04 11:59:49 +0000113 timeout=http_timeout, follow_redirects=follow_redirects)
Matthew Treinish74514402016-09-01 11:44:57 -0400114 else:
115 self.http_obj = http.ClosingHttp(
116 disable_ssl_certificate_validation=dscv, ca_certs=ca_certs,
Jens Harbott3ffa54e2018-07-04 11:59:49 +0000117 timeout=http_timeout, follow_redirects=follow_redirects)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500118
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500119 def get_headers(self, accept_type=None, send_type=None):
120 """Return the default headers which will be used with outgoing requests
121
122 :param str accept_type: The media type to use for the Accept header, if
123 one isn't provided the object var TYPE will be
124 used
125 :param str send_type: The media-type to use for the Content-Type
126 header, if one isn't provided the object var
127 TYPE will be used
128 :rtype: dict
129 :return: The dictionary of headers which can be used in the headers
130 dict for outgoing request
131 """
132 if accept_type is None:
Masayuki Igawa189b92f2017-04-24 18:57:17 +0900133 accept_type = 'json'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500134 if send_type is None:
Masayuki Igawa189b92f2017-04-24 18:57:17 +0900135 send_type = 'json'
Ilya Shakhat1291bb42017-11-29 18:08:16 +0100136 headers = {'Content-Type': 'application/%s' % send_type,
137 'Accept': 'application/%s' % accept_type}
138 headers.update(profiler.serialize_as_http_headers())
139 return headers
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500140
141 def __str__(self):
142 STRING_LIMIT = 80
143 str_format = ("service:%s, base_url:%s, "
144 "filters: %s, build_interval:%s, build_timeout:%s"
145 "\ntoken:%s..., \nheaders:%s...")
146 return str_format % (self.service, self.base_url,
147 self.filters, self.build_interval,
148 self.build_timeout,
149 str(self.token)[0:STRING_LIMIT],
150 str(self.get_headers())[0:STRING_LIMIT])
151
152 @property
153 def user(self):
154 """The username used for requests
155
156 :rtype: string
157 :return: The username being used for requests
158 """
159
160 return self.auth_provider.credentials.username
161
162 @property
163 def user_id(self):
164 """The user_id used for requests
165
166 :rtype: string
167 :return: The user id being used for requests
168 """
169 return self.auth_provider.credentials.user_id
170
171 @property
172 def tenant_name(self):
173 """The tenant/project being used for requests
174
175 :rtype: string
176 :return: The tenant/project name being used for requests
177 """
178 return self.auth_provider.credentials.tenant_name
179
180 @property
Rodolfo Alonso Hernandezc1449d42020-02-15 13:24:28 +0000181 def project_id(self):
182 """The project id being used for requests
183
184 :rtype: string
185 :return: The project id being used for requests
186 """
187 return self.auth_provider.credentials.tenant_id
188
189 @property
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500190 def tenant_id(self):
191 """The tenant/project id being used for requests
192
193 :rtype: string
194 :return: The tenant/project id being used for requests
195 """
Rodolfo Alonso Hernandezc1449d42020-02-15 13:24:28 +0000196 # NOTE(ralonsoh): this property should be deprecated, reference
197 # blueprint adopt-oslo-versioned-objects-for-db.
198 versionutils.report_deprecated_feature(
199 self.LOG, '"tenant_id" property is deprecated for removal, use '
200 '"project_id" instead')
201 return self.project_id
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500202
203 @property
204 def password(self):
205 """The password being used for requests
206
207 :rtype: string
208 :return: The password being used for requests
209 """
210 return self.auth_provider.credentials.password
211
212 @property
213 def base_url(self):
214 return self.auth_provider.base_url(filters=self.filters)
215
216 @property
217 def token(self):
218 return self.auth_provider.get_token()
219
220 @property
221 def filters(self):
222 _filters = dict(
223 service=self.service,
224 endpoint_type=self.endpoint_type,
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -0600225 region=self.region,
226 name=self.name
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500227 )
228 if self.api_version is not None:
229 _filters['api_version'] = self.api_version
230 if self._skip_path:
231 _filters['skip_path'] = self._skip_path
232 return _filters
233
234 def skip_path(self):
235 """When set, ignore the path part of the base URL from the catalog"""
236 self._skip_path = True
237
238 def reset_path(self):
239 """When reset, use the base URL from the catalog as-is"""
240 self._skip_path = False
241
242 @classmethod
243 def expected_success(cls, expected_code, read_code):
244 """Check expected success response code against the http response
245
246 :param int expected_code: The response code that is expected.
247 Optionally a list of integers can be used
248 to specify multiple valid success codes
249 :param int read_code: The response code which was returned in the
250 response
251 :raises AssertionError: if the expected_code isn't a valid http success
252 response code
253 :raises exceptions.InvalidHttpSuccessCode: if the read code isn't an
254 expected http success code
255 """
ghanshyamc3074202016-04-18 15:20:45 +0900256 if not isinstance(read_code, int):
257 raise TypeError("'read_code' must be an int instead of (%s)"
258 % type(read_code))
259
Hanxi2f977db2016-09-01 17:31:28 +0800260 assert_msg = ("This function only allowed to use for HTTP status "
261 "codes which explicitly defined in the RFC 7231 & 4918. "
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500262 "{0} is not a defined Success Code!"
263 ).format(expected_code)
264 if isinstance(expected_code, list):
265 for code in expected_code:
266 assert code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg
267 else:
268 assert expected_code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg
269
270 # NOTE(afazekas): the http status code above 400 is processed by
271 # the _error_checker method
272 if read_code < 400:
zhufl4d2f5152017-01-17 11:16:12 +0800273 pattern = ("Unexpected http success status code {0}, "
274 "The expected status code is {1}")
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500275 if ((not isinstance(expected_code, list) and
276 (read_code != expected_code)) or
277 (isinstance(expected_code, list) and
278 (read_code not in expected_code))):
279 details = pattern.format(read_code, expected_code)
280 raise exceptions.InvalidHttpSuccessCode(details)
281
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200282 def post(self, url, body, headers=None, extra_headers=False,
283 chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500284 """Send a HTTP POST request using keystone auth
285
286 :param str url: the relative url to send the post request to
287 :param dict body: the request body
288 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300289 :param bool extra_headers: Boolean value than indicates if the headers
290 returned by the get_headers() method are to
291 be used but additional headers are needed in
292 the request pass them in as a dict.
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200293 :param bool chunked: sends the body with chunked encoding
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 """
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200298 return self.request('POST', url, extra_headers, headers, body, chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500299
300 def get(self, url, headers=None, extra_headers=False):
301 """Send a HTTP GET 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 get request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500304 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300305 :param bool extra_headers: Boolean value than indicates if the headers
306 returned by the get_headers() method are to
307 be used but additional headers are needed in
308 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500309 :return: a tuple with the first entry containing the response headers
310 and the second the response body
311 :rtype: tuple
312 """
313 return self.request('GET', url, extra_headers, headers)
314
315 def delete(self, url, headers=None, body=None, extra_headers=False):
316 """Send a HTTP DELETE request using keystone service catalog and auth
317
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530318 :param str url: the relative url to send the delete request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500319 :param dict headers: The headers to use for the request
320 :param dict body: the request body
vsaienko9eb846b2016-04-09 00:35:47 +0300321 :param bool extra_headers: Boolean value than indicates if the headers
322 returned by the get_headers() method are to
323 be used but additional headers are needed in
324 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500325 :return: a tuple with the first entry containing the response headers
326 and the second the response body
327 :rtype: tuple
328 """
329 return self.request('DELETE', url, extra_headers, headers, body)
330
331 def patch(self, url, body, headers=None, extra_headers=False):
332 """Send a HTTP PATCH request using keystone service catalog and auth
333
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530334 :param str url: the relative url to send the patch request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500335 :param dict body: the request body
336 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300337 :param bool extra_headers: Boolean value than indicates if the headers
338 returned by the get_headers() method are to
339 be used but additional headers are needed in
340 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500341 :return: a tuple with the first entry containing the response headers
342 and the second the response body
343 :rtype: tuple
344 """
345 return self.request('PATCH', url, extra_headers, headers, body)
346
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200347 def put(self, url, body, headers=None, extra_headers=False, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500348 """Send a HTTP PUT request using keystone service catalog and auth
349
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530350 :param str url: the relative url to send the put request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500351 :param dict body: the request body
352 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300353 :param bool extra_headers: Boolean value than indicates if the headers
354 returned by the get_headers() method are to
355 be used but additional headers are needed in
356 the request pass them in as a dict.
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200357 :param bool chunked: sends the body with chunked encoding
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 """
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200362 return self.request('PUT', url, extra_headers, headers, body, chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500363
364 def head(self, url, headers=None, extra_headers=False):
365 """Send a HTTP HEAD 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 head 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('HEAD', url, extra_headers, headers)
378
379 def copy(self, url, headers=None, extra_headers=False):
380 """Send a HTTP COPY request using keystone service catalog and auth
381
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530382 :param str url: the relative url to send the copy request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500383 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300384 :param bool extra_headers: Boolean value than indicates if the headers
385 returned by the get_headers() method are to
386 be used but additional headers are needed in
387 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500388 :return: a tuple with the first entry containing the response headers
389 and the second the response body
390 :rtype: tuple
391 """
392 return self.request('COPY', url, extra_headers, headers)
393
394 def get_versions(self):
sunqingliang699690f62018-11-09 15:03:17 +0800395 """Get the versions on an endpoint from the keystone catalog
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500396
397 This method will make a GET request on the baseurl from the keystone
398 catalog to return a list of API versions. It is expected that a GET
399 on the endpoint in the catalog will return a list of supported API
400 versions.
401
junboli872ca872017-07-21 13:24:38 +0800402 :return: tuple with response headers and list of version numbers
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500403 :rtype: tuple
404 """
405 resp, body = self.get('')
406 body = self._parse_resp(body)
407 versions = map(lambda x: x['id'], body)
408 return resp, versions
409
410 def _get_request_id(self, resp):
411 for i in ('x-openstack-request-id', 'x-compute-request-id'):
412 if i in resp:
413 return resp[i]
414 return ""
415
416 def _safe_body(self, body, maxlen=4096):
417 # convert a structure into a string safely
418 try:
419 text = six.text_type(body)
420 except UnicodeDecodeError:
421 # if this isn't actually text, return marker that
422 return "<BinaryData: removed>"
423 if len(text) > maxlen:
424 return text[:maxlen]
425 else:
426 return text
427
guo yunxian9f749f92016-08-25 10:55:04 +0800428 def _log_request_start(self, method, req_url):
Jordan Pittier9e227c52016-02-09 14:35:18 +0100429 caller_name = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500430 if self.trace_requests and re.search(self.trace_requests, caller_name):
Jordan Pittier525ec712016-12-07 17:51:26 +0100431 self.LOG.debug('Starting Request (%s): %s %s', caller_name,
432 method, req_url)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500433
guo yunxian9f749f92016-08-25 10:55:04 +0800434 def _log_request_full(self, resp, req_headers=None, req_body=None,
435 resp_body=None, extra=None):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500436 if 'X-Auth-Token' in req_headers:
437 req_headers['X-Auth-Token'] = '<omitted>'
Ken'ichi Ohmichi2902a7b2018-07-14 02:31:03 +0000438 if 'X-Subject-Token' in req_headers:
439 req_headers['X-Subject-Token'] = '<omitted>'
Andrea Frittoli (andreaf)a1edb2d2016-05-10 16:09:59 +0100440 # A shallow copy is sufficient
441 resp_log = resp.copy()
442 if 'x-subject-token' in resp_log:
443 resp_log['x-subject-token'] = '<omitted>'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500444 log_fmt = """Request - Headers: %s
445 Body: %s
446 Response - Headers: %s
447 Body: %s"""
448
449 self.LOG.debug(
Jordan Pittier525ec712016-12-07 17:51:26 +0100450 log_fmt,
451 str(req_headers),
452 self._safe_body(req_body),
453 str(resp_log),
454 self._safe_body(resp_body),
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500455 extra=extra)
456
457 def _log_request(self, method, req_url, resp,
458 secs="", req_headers=None,
459 req_body=None, resp_body=None):
460 if req_headers is None:
461 req_headers = {}
462 # if we have the request id, put it in the right part of the log
463 extra = dict(request_id=self._get_request_id(resp))
464 # NOTE(sdague): while we still have 6 callers to this function
465 # we're going to just provide work around on who is actually
466 # providing timings by gracefully adding no content if they don't.
467 # Once we're down to 1 caller, clean this up.
Jordan Pittier9e227c52016-02-09 14:35:18 +0100468 caller_name = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500469 if secs:
470 secs = " %.3fs" % secs
471 self.LOG.info(
Jordan Pittier525ec712016-12-07 17:51:26 +0100472 'Request (%s): %s %s %s%s',
473 caller_name,
474 resp['status'],
475 method,
476 req_url,
477 secs,
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500478 extra=extra)
479
480 # Also look everything at DEBUG if you want to filter this
481 # out, don't run at debug.
Anusha Raminenif3eb9472017-01-13 08:54:01 +0530482 if self.LOG.isEnabledFor(logging.DEBUG):
guo yunxian9f749f92016-08-25 10:55:04 +0800483 self._log_request_full(resp, req_headers, req_body,
484 resp_body, extra)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500485
486 def _parse_resp(self, body):
487 try:
488 body = json.loads(body)
489 except ValueError:
490 return body
491
492 # We assume, that if the first value of the deserialized body's
493 # item set is a dict or a list, that we just return the first value
494 # of deserialized body.
495 # Essentially "cutting out" the first placeholder element in a body
496 # that looks like this:
497 #
498 # {
499 # "users": [
500 # ...
501 # ]
502 # }
503 try:
504 # Ensure there are not more than one top-level keys
505 # NOTE(freerunner): Ensure, that JSON is not nullable to
506 # to prevent StopIteration Exception
Ken'ichi Ohmichi69a8edc2017-04-28 11:41:20 -0700507 if not hasattr(body, "keys") or len(body.keys()) != 1:
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500508 return body
509 # Just return the "wrapped" element
zhufl3ead9982020-11-19 14:39:04 +0800510 _, first_item = six.next(six.iteritems(body))
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500511 if isinstance(first_item, (dict, list)):
512 return first_item
513 except (ValueError, IndexError):
514 pass
515 return body
516
517 def response_checker(self, method, resp, resp_body):
518 """A sanity check on the response from a HTTP request
519
520 This method does a sanity check on whether the response from an HTTP
521 request conforms the HTTP RFC.
522
523 :param str method: The HTTP verb of the request associated with the
524 response being passed in.
525 :param resp: The response headers
526 :param resp_body: The body of the response
527 :raises ResponseWithNonEmptyBody: If the response with the status code
528 is not supposed to have a body
529 :raises ResponseWithEntity: If the response code is 205 but has an
530 entity
531 """
532 if (resp.status in set((204, 205, 304)) or resp.status < 200 or
533 method.upper() == 'HEAD') and resp_body:
534 raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
535 # NOTE(afazekas):
536 # If the HTTP Status Code is 205
537 # 'The response MUST NOT include an entity.'
538 # A HTTP entity has an entity-body and an 'entity-header'.
539 # In the HTTP response specification (Section 6) the 'entity-header'
540 # 'generic-header' and 'response-header' are in OR relation.
541 # All headers not in the above two group are considered as entity
542 # header in every interpretation.
543
544 if (resp.status == 205 and
545 0 != len(set(resp.keys()) - set(('status',)) -
546 self.response_header_lc - self.general_header_lc)):
Matt Riedemann91d92422019-01-29 16:19:49 -0500547 raise exceptions.ResponseWithEntity()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500548 # NOTE(afazekas)
549 # Now the swift sometimes (delete not empty container)
550 # returns with non json error response, we can create new rest class
551 # for swift.
552 # Usually RFC2616 says error responses SHOULD contain an explanation.
553 # The warning is normal for SHOULD/SHOULD NOT case
554
555 # Likely it will cause an error
556 if method != 'HEAD' and not resp_body and resp.status >= 400:
557 self.LOG.warning("status >= 400 response with empty body")
558
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200559 def _request(self, method, url, headers=None, body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500560 """A simple HTTP request interface."""
561 # Authenticate the request with the auth provider
562 req_url, req_headers, req_body = self.auth_provider.auth_request(
563 method, url, headers, body, self.filters)
564
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500565 resp, resp_body = self.raw_request(
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200566 req_url, method, headers=req_headers, body=req_body,
567 chunked=chunked
568 )
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500569 # Verify HTTP response codes
570 self.response_checker(method, resp, resp_body)
571
572 return resp, resp_body
573
Ghanshyam Mann47a41992019-10-02 16:56:26 +0000574 def raw_request(self, url, method, headers=None, body=None, chunked=False,
575 log_req_body=None):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500576 """Send a raw HTTP request without the keystone catalog or auth
577
578 This method sends a HTTP request in the same manner as the request()
579 method, however it does so without using keystone auth or the catalog
580 to determine the base url. Additionally no response handling is done
581 the results from the request are just returned.
582
583 :param str url: Full url to send the request
584 :param str method: The HTTP verb to use for the request
zhuflcf35f652018-08-17 10:13:43 +0800585 :param dict headers: Headers to use for the request. If none are
586 specified, then the headers returned from the
587 get_headers() method are used. If the request
588 explicitly requires no headers use an empty dict.
Anh Trand44a8be2016-03-25 09:49:14 +0700589 :param str body: Body to send with the request
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200590 :param bool chunked: sends the body with chunked encoding
Ghanshyam Mann47a41992019-10-02 16:56:26 +0000591 :param str log_req_body: Whether to log the request body or not.
592 It is default to None which means request
593 body is safe to log otherwise pass any string
594 you want to log in place of request body.
595 For example: '<omitted>'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500596 :rtype: tuple
597 :return: a tuple with the first entry containing the response headers
598 and the second the response body
599 """
600 if headers is None:
601 headers = self.get_headers()
Ghanshyam Manncb3cf032019-09-26 00:02:54 +0000602 # Do the actual request, and time it
603 start = time.time()
604 self._log_request_start(method, url)
605 resp, resp_body = self.http_obj.request(
606 url, method, headers=headers,
607 body=body, chunked=chunked)
608 end = time.time()
Ghanshyam Mann47a41992019-10-02 16:56:26 +0000609 req_body = body if log_req_body is None else log_req_body
Ghanshyam Manncb3cf032019-09-26 00:02:54 +0000610 self._log_request(method, url, resp, secs=(end - start),
Ghanshyam Mann47a41992019-10-02 16:56:26 +0000611 req_headers=headers, req_body=req_body,
Ghanshyam Manncb3cf032019-09-26 00:02:54 +0000612 resp_body=resp_body)
613 return resp, resp_body
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500614
615 def request(self, method, url, extra_headers=False, headers=None,
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200616 body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500617 """Send a HTTP request with keystone auth and using the catalog
618
619 This method will send an HTTP request using keystone auth in the
620 headers and the catalog to determine the endpoint to use for the
621 baseurl to send the request to. Additionally
622
623 When a response is received it will check it to see if an error
624 response was received. If it was an exception will be raised to enable
625 it to be handled quickly.
626
627 This method will also handle rate-limiting, if a 413 response code is
628 received it will retry the request after waiting the 'retry-after'
629 duration from the header.
630
631 :param str method: The HTTP verb to use for the request
632 :param str url: Relative url to send the request to
vsaienko9eb846b2016-04-09 00:35:47 +0300633 :param bool extra_headers: Boolean value than indicates if the headers
634 returned by the get_headers() method are to
635 be used but additional headers are needed in
636 the request pass them in as a dict.
zhuflcf35f652018-08-17 10:13:43 +0800637 :param dict headers: Headers to use for the request. If none are
638 specified, then the headers returned from the
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500639 get_headers() method are used. If the request
640 explicitly requires no headers use an empty dict.
Anh Trand44a8be2016-03-25 09:49:14 +0700641 :param str body: Body to send with the request
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200642 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500643 :rtype: tuple
644 :return: a tuple with the first entry containing the response headers
645 and the second the response body
646 :raises UnexpectedContentType: If the content-type of the response
647 isn't an expect type
648 :raises Unauthorized: If a 401 response code is received
649 :raises Forbidden: If a 403 response code is received
650 :raises NotFound: If a 404 response code is received
651 :raises BadRequest: If a 400 response code is received
652 :raises Gone: If a 410 response code is received
653 :raises Conflict: If a 409 response code is received
Kevin Bentona82bc862017-02-13 01:16:13 -0800654 :raises PreconditionFailed: If a 412 response code is received
zhuflcf35f652018-08-17 10:13:43 +0800655 :raises OverLimit: If a 413 response code is received and retry-after
656 is not in the response body or its retry operation
657 exceeds the limits defined by the server
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500658 :raises RateLimitExceeded: If a 413 response code is received and
zhuflcf35f652018-08-17 10:13:43 +0800659 retry-after is in the response body and
660 its retry operation does not exceeds the
661 limits defined by the server
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500662 :raises InvalidContentType: If a 415 response code is received
663 :raises UnprocessableEntity: If a 422 response code is received
664 :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
665 and couldn't be parsed
666 :raises NotImplemented: If a 501 response code is received
667 :raises ServerFault: If a 500 response code is received
668 :raises UnexpectedResponseCode: If a response code above 400 is
669 received and it doesn't fall into any
670 of the handled checks
671 """
672 # if extra_headers is True
673 # default headers would be added to headers
674 retry = 0
675
676 if headers is None:
677 # NOTE(vponomaryov): if some client do not need headers,
678 # it should explicitly pass empty dict
679 headers = self.get_headers()
680 elif extra_headers:
681 try:
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500682 headers.update(self.get_headers())
683 except (ValueError, TypeError):
684 headers = self.get_headers()
685
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200686 resp, resp_body = self._request(method, url, headers=headers,
687 body=body, chunked=chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500688
689 while (resp.status == 413 and
690 'retry-after' in resp and
691 not self.is_absolute_limit(
692 resp, self._parse_resp(resp_body)) and
693 retry < MAX_RECURSION_DEPTH):
694 retry += 1
Paul Glass119565a2016-04-06 11:41:42 -0500695 delay = self._get_retry_after_delay(resp)
696 self.LOG.debug(
697 "Sleeping %s seconds based on retry-after header", delay
698 )
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500699 time.sleep(delay)
700 resp, resp_body = self._request(method, url,
701 headers=headers, body=body)
Ken'ichi Ohmichi17051e82016-10-20 11:46:06 -0700702 self._error_checker(resp, resp_body)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500703 return resp, resp_body
704
Paul Glass119565a2016-04-06 11:41:42 -0500705 def _get_retry_after_delay(self, resp):
706 """Extract the delay from the retry-after header.
707
708 This supports both integer and HTTP date formatted retry-after headers
709 per RFC 2616.
710
711 :param resp: The response containing the retry-after headers
712 :rtype: int
713 :return: The delay in seconds, clamped to be at least 1 second
714 :raises ValueError: On failing to parse the delay
715 """
716 delay = None
717 try:
718 delay = int(resp['retry-after'])
719 except (ValueError, KeyError):
720 pass
721
722 try:
723 retry_timestamp = self._parse_http_date(resp['retry-after'])
724 date_timestamp = self._parse_http_date(resp['date'])
725 delay = int(retry_timestamp - date_timestamp)
726 except (ValueError, OverflowError, KeyError):
727 pass
728
729 if delay is None:
730 raise ValueError(
731 "Failed to parse retry-after header %r as either int or "
732 "HTTP-date." % resp.get('retry-after')
733 )
734
735 # Retry-after headers do not have sub-second precision. Clients may
736 # receive a delay of 0. After sleeping 0 seconds, we would (likely) hit
737 # another 413. To avoid this, always sleep at least 1 second.
738 return max(1, delay)
739
740 def _parse_http_date(self, val):
741 """Parse an HTTP date, like 'Fri, 31 Dec 1999 23:59:59 GMT'.
742
743 Return an epoch timestamp (float), as returned by time.mktime().
744 """
745 parts = email.utils.parsedate(val)
746 if not parts:
747 raise ValueError("Failed to parse date %s" % val)
748 return time.mktime(parts)
749
Ken'ichi Ohmichi17051e82016-10-20 11:46:06 -0700750 def _error_checker(self, resp, resp_body):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500751
752 # NOTE(mtreinish): Check for httplib response from glance_http. The
753 # object can't be used here because importing httplib breaks httplib2.
754 # If another object from a class not imported were passed here as
755 # resp this could possibly fail
756 if str(type(resp)) == "<type 'instance'>":
757 ctype = resp.getheader('content-type')
758 else:
759 try:
760 ctype = resp['content-type']
761 # NOTE(mtreinish): Keystone delete user responses doesn't have a
762 # content-type header. (They don't have a body) So just pretend it
763 # is set.
764 except KeyError:
765 ctype = 'application/json'
766
767 # It is not an error response
768 if resp.status < 400:
769 return
770
zhipenghd1db0c72017-02-21 04:40:07 -0500771 # NOTE(zhipengh): There is a purposefully duplicate of content-type
772 # with the only difference is with or without spaces, as specified
773 # in RFC7231.
774 JSON_ENC = ['application/json', 'application/json; charset=utf-8',
775 'application/json;charset=utf-8']
776
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500777 # NOTE(mtreinish): This is for compatibility with Glance and swift
778 # APIs. These are the return content types that Glance api v1
779 # (and occasionally swift) are using.
zhipenghd1db0c72017-02-21 04:40:07 -0500780 # NOTE(zhipengh): There is a purposefully duplicate of content-type
781 # with the only difference is with or without spaces, as specified
782 # in RFC7231.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500783 TXT_ENC = ['text/plain', 'text/html', 'text/html; charset=utf-8',
zhipenghd1db0c72017-02-21 04:40:07 -0500784 'text/plain; charset=utf-8', 'text/html;charset=utf-8',
785 'text/plain;charset=utf-8']
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500786
787 if ctype.lower() in JSON_ENC:
788 parse_resp = True
789 elif ctype.lower() in TXT_ENC:
790 parse_resp = False
791 else:
792 raise exceptions.UnexpectedContentType(str(resp.status),
793 resp=resp)
794
795 if resp.status == 401:
796 if parse_resp:
797 resp_body = self._parse_resp(resp_body)
798 raise exceptions.Unauthorized(resp_body, resp=resp)
799
800 if resp.status == 403:
801 if parse_resp:
802 resp_body = self._parse_resp(resp_body)
803 raise exceptions.Forbidden(resp_body, resp=resp)
804
805 if resp.status == 404:
806 if parse_resp:
807 resp_body = self._parse_resp(resp_body)
808 raise exceptions.NotFound(resp_body, resp=resp)
809
810 if resp.status == 400:
811 if parse_resp:
812 resp_body = self._parse_resp(resp_body)
813 raise exceptions.BadRequest(resp_body, resp=resp)
814
815 if resp.status == 410:
816 if parse_resp:
817 resp_body = self._parse_resp(resp_body)
818 raise exceptions.Gone(resp_body, resp=resp)
819
820 if resp.status == 409:
821 if parse_resp:
822 resp_body = self._parse_resp(resp_body)
823 raise exceptions.Conflict(resp_body, resp=resp)
824
Kevin Bentona82bc862017-02-13 01:16:13 -0800825 if resp.status == 412:
826 if parse_resp:
827 resp_body = self._parse_resp(resp_body)
828 raise exceptions.PreconditionFailed(resp_body, resp=resp)
829
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500830 if resp.status == 413:
831 if parse_resp:
832 resp_body = self._parse_resp(resp_body)
833 if self.is_absolute_limit(resp, resp_body):
834 raise exceptions.OverLimit(resp_body, resp=resp)
835 else:
836 raise exceptions.RateLimitExceeded(resp_body, resp=resp)
837
838 if resp.status == 415:
839 if parse_resp:
840 resp_body = self._parse_resp(resp_body)
841 raise exceptions.InvalidContentType(resp_body, resp=resp)
842
843 if resp.status == 422:
844 if parse_resp:
845 resp_body = self._parse_resp(resp_body)
846 raise exceptions.UnprocessableEntity(resp_body, resp=resp)
847
848 if resp.status in (500, 501):
849 message = resp_body
850 if parse_resp:
851 try:
852 resp_body = self._parse_resp(resp_body)
853 except ValueError:
854 # If response body is a non-json string message.
855 # Use resp_body as is and raise InvalidResponseBody
856 # exception.
857 raise exceptions.InvalidHTTPResponseBody(message)
858 else:
859 if isinstance(resp_body, dict):
860 # I'm seeing both computeFault
861 # and cloudServersFault come back.
862 # Will file a bug to fix, but leave as is for now.
863 if 'cloudServersFault' in resp_body:
864 message = resp_body['cloudServersFault']['message']
865 elif 'computeFault' in resp_body:
866 message = resp_body['computeFault']['message']
867 elif 'error' in resp_body:
868 message = resp_body['error']['message']
869 elif 'message' in resp_body:
870 message = resp_body['message']
871 else:
872 message = resp_body
873
874 if resp.status == 501:
875 raise exceptions.NotImplemented(resp_body, resp=resp,
876 message=message)
877 else:
878 raise exceptions.ServerFault(resp_body, resp=resp,
879 message=message)
880
881 if resp.status >= 400:
882 raise exceptions.UnexpectedResponseCode(str(resp.status),
883 resp=resp)
884
885 def is_absolute_limit(self, resp, resp_body):
886 if (not isinstance(resp_body, collections.Mapping) or
887 'retry-after' not in resp):
888 return True
Paul Glass119565a2016-04-06 11:41:42 -0500889 return 'exceed' in resp_body.get('message', 'blabla')
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500890
891 def wait_for_resource_deletion(self, id):
892 """Waits for a resource to be deleted
893
894 This method will loop over is_resource_deleted until either
895 is_resource_deleted returns True or the build timeout is reached. This
896 depends on is_resource_deleted being implemented
897
898 :param str id: The id of the resource to check
899 :raises TimeoutException: If the build_timeout has elapsed and the
900 resource still hasn't been deleted
901 """
902 start_time = int(time.time())
903 while True:
904 if self.is_resource_deleted(id):
905 return
906 if int(time.time()) - start_time >= self.build_timeout:
907 message = ('Failed to delete %(resource_type)s %(id)s within '
908 'the required time (%(timeout)s s).' %
909 {'resource_type': self.resource_type, 'id': id,
910 'timeout': self.build_timeout})
Jordan Pittier9e227c52016-02-09 14:35:18 +0100911 caller = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500912 if caller:
913 message = '(%s) %s' % (caller, message)
914 raise exceptions.TimeoutException(message)
915 time.sleep(self.build_interval)
916
Abhishek Kekane7cff1302020-07-16 10:30:13 +0000917 def wait_for_resource_activation(self, id):
918 """Waits for a resource to become active
919
920 This method will loop over is_resource_active until either
921 is_resource_active returns True or the build timeout is reached. This
922 depends on is_resource_active being implemented
923
924 :param str id: The id of the resource to check
925 :raises TimeoutException: If the build_timeout has elapsed and the
926 resource still hasn't been active
927 """
928 start_time = int(time.time())
929 while True:
930 if self.is_resource_active(id):
931 return
932 if int(time.time()) - start_time >= self.build_timeout:
933 message = ('Failed to reach active state %(resource_type)s '
934 '%(id)s within the required time (%(timeout)s s).' %
935 {'resource_type': self.resource_type, 'id': id,
936 'timeout': self.build_timeout})
937 caller = test_utils.find_test_caller()
938 if caller:
939 message = '(%s) %s' % (caller, message)
940 raise exceptions.TimeoutException(message)
941 time.sleep(self.build_interval)
942
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500943 def is_resource_deleted(self, id):
944 """Subclasses override with specific deletion detection."""
945 message = ('"%s" does not implement is_resource_deleted'
946 % self.__class__.__name__)
947 raise NotImplementedError(message)
948
Abhishek Kekane7cff1302020-07-16 10:30:13 +0000949 def is_resource_active(self, id):
950 """Subclasses override with specific active detection."""
951 message = ('"%s" does not implement is_resource_active'
952 % self.__class__.__name__)
953 raise NotImplementedError(message)
954
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500955 @property
956 def resource_type(self):
957 """Returns the primary type of resource this client works with."""
958 return 'resource'
959
960 @classmethod
961 def validate_response(cls, schema, resp, body):
962 # Only check the response if the status code is a success code
963 # TODO(cyeoh): Eventually we should be able to verify that a failure
964 # code if it exists is something that we expect. This is explicitly
965 # declared in the V3 API and so we should be able to export this in
966 # the response schema. For now we'll ignore it.
967 if resp.status in HTTP_SUCCESS + HTTP_REDIRECTION:
968 cls.expected_success(schema['status_code'], resp.status)
969
970 # Check the body of a response
971 body_schema = schema.get('response_body')
972 if body_schema:
973 try:
974 jsonschema.validate(body, body_schema,
975 cls=JSONSCHEMA_VALIDATOR,
976 format_checker=FORMAT_CHECKER)
977 except jsonschema.ValidationError as ex:
guo yunxiana3f55282016-08-10 14:35:16 +0800978 msg = ("HTTP response body is invalid (%s)" % ex)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500979 raise exceptions.InvalidHTTPResponseBody(msg)
980 else:
981 if body:
guo yunxiana3f55282016-08-10 14:35:16 +0800982 msg = ("HTTP response body should not exist (%s)" % body)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500983 raise exceptions.InvalidHTTPResponseBody(msg)
984
985 # Check the header of a response
986 header_schema = schema.get('response_header')
987 if header_schema:
988 try:
989 jsonschema.validate(resp, header_schema,
990 cls=JSONSCHEMA_VALIDATOR,
991 format_checker=FORMAT_CHECKER)
992 except jsonschema.ValidationError as ex:
guo yunxiana3f55282016-08-10 14:35:16 +0800993 msg = ("HTTP response header is invalid (%s)" % ex)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500994 raise exceptions.InvalidHTTPResponseHeader(msg)
995
Ken'ichi Ohmichida26b162017-03-03 15:53:46 -0800996 def _get_base_version_url(self):
997 # TODO(oomichi): This method can be used for auth's replace_version().
998 # So it is nice to have common logic for the maintenance.
999 endpoint = self.base_url
1000 url = urllib.parse.urlsplit(endpoint)
1001 new_path = re.split(r'(^|/)+v\d+(\.\d+)?', url.path)[0]
1002 url = list(url)
1003 url[2] = new_path + '/'
1004 return urllib.parse.urlunsplit(url)
1005
Matthew Treinish9e26ca82016-02-23 11:43:20 -05001006
1007class ResponseBody(dict):
1008 """Class that wraps an http response and dict body into a single value.
1009
1010 Callers that receive this object will normally use it as a dict but
1011 can extract the response if needed.
1012 """
1013
1014 def __init__(self, response, body=None):
1015 body_data = body or {}
1016 self.update(body_data)
1017 self.response = response
1018
1019 def __str__(self):
1020 body = super(ResponseBody, self).__str__()
1021 return "response: %s\nBody: %s" % (self.response, body)
1022
1023
1024class ResponseBodyData(object):
1025 """Class that wraps an http response and string data into a single value.
1026
1027 """
1028
1029 def __init__(self, response, data):
1030 self.response = response
1031 self.data = data
1032
1033 def __str__(self):
1034 return "response: %s\nBody: %s" % (self.response, self.data)
1035
1036
1037class ResponseBodyList(list):
1038 """Class that wraps an http response and list body into a single value.
1039
1040 Callers that receive this object will normally use it as a list but
1041 can extract the response if needed.
1042 """
1043
1044 def __init__(self, response, body=None):
1045 body_data = body or []
1046 self.extend(body_data)
1047 self.response = response
1048
1049 def __str__(self):
1050 body = super(ResponseBodyList, self).__str__()
1051 return "response: %s\nBody: %s" % (self.response, body)