blob: c950f6382a04b5c2fcb01fddbfbd9ad5a6529e74 [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
songwenping99d6e002021-01-05 03:07:46 +000021import urllib
Matthew Treinish9e26ca82016-02-23 11:43:20 -050022
23import jsonschema
24from oslo_log import log as logging
Rodolfo Alonso Hernandezc1449d42020-02-15 13:24:28 +000025from oslo_log import versionutils
Matthew Treinish9e26ca82016-02-23 11:43:20 -050026from oslo_serialization import jsonutils as json
27import six
28
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'))
zhufl8464cef2020-12-11 10:51:25 +0800107 self.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,
zhufl8464cef2020-12-11 10:51:25 +0800112 disable_ssl_certificate_validation=self.dscv,
113 ca_certs=ca_certs,
Jens Harbott3ffa54e2018-07-04 11:59:49 +0000114 timeout=http_timeout, follow_redirects=follow_redirects)
Matthew Treinish74514402016-09-01 11:44:57 -0400115 else:
116 self.http_obj = http.ClosingHttp(
zhufl8464cef2020-12-11 10:51:25 +0800117 disable_ssl_certificate_validation=self.dscv,
118 ca_certs=ca_certs,
Jens Harbott3ffa54e2018-07-04 11:59:49 +0000119 timeout=http_timeout, follow_redirects=follow_redirects)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500120
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500121 def get_headers(self, accept_type=None, send_type=None):
122 """Return the default headers which will be used with outgoing requests
123
124 :param str accept_type: The media type to use for the Accept header, if
125 one isn't provided the object var TYPE will be
126 used
127 :param str send_type: The media-type to use for the Content-Type
128 header, if one isn't provided the object var
129 TYPE will be used
130 :rtype: dict
131 :return: The dictionary of headers which can be used in the headers
132 dict for outgoing request
133 """
134 if accept_type is None:
Masayuki Igawa189b92f2017-04-24 18:57:17 +0900135 accept_type = 'json'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500136 if send_type is None:
Masayuki Igawa189b92f2017-04-24 18:57:17 +0900137 send_type = 'json'
Ilya Shakhat1291bb42017-11-29 18:08:16 +0100138 headers = {'Content-Type': 'application/%s' % send_type,
139 'Accept': 'application/%s' % accept_type}
140 headers.update(profiler.serialize_as_http_headers())
141 return headers
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500142
143 def __str__(self):
144 STRING_LIMIT = 80
145 str_format = ("service:%s, base_url:%s, "
146 "filters: %s, build_interval:%s, build_timeout:%s"
147 "\ntoken:%s..., \nheaders:%s...")
148 return str_format % (self.service, self.base_url,
149 self.filters, self.build_interval,
150 self.build_timeout,
151 str(self.token)[0:STRING_LIMIT],
152 str(self.get_headers())[0:STRING_LIMIT])
153
154 @property
155 def user(self):
156 """The username used for requests
157
158 :rtype: string
159 :return: The username being used for requests
160 """
161
162 return self.auth_provider.credentials.username
163
164 @property
165 def user_id(self):
166 """The user_id used for requests
167
168 :rtype: string
169 :return: The user id being used for requests
170 """
171 return self.auth_provider.credentials.user_id
172
173 @property
174 def tenant_name(self):
175 """The tenant/project being used for requests
176
177 :rtype: string
178 :return: The tenant/project name being used for requests
179 """
180 return self.auth_provider.credentials.tenant_name
181
182 @property
Rodolfo Alonso Hernandezc1449d42020-02-15 13:24:28 +0000183 def project_id(self):
184 """The project id being used for requests
185
186 :rtype: string
187 :return: The project id being used for requests
188 """
189 return self.auth_provider.credentials.tenant_id
190
191 @property
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500192 def tenant_id(self):
193 """The tenant/project id being used for requests
194
195 :rtype: string
196 :return: The tenant/project id being used for requests
197 """
Rodolfo Alonso Hernandezc1449d42020-02-15 13:24:28 +0000198 # NOTE(ralonsoh): this property should be deprecated, reference
199 # blueprint adopt-oslo-versioned-objects-for-db.
200 versionutils.report_deprecated_feature(
201 self.LOG, '"tenant_id" property is deprecated for removal, use '
202 '"project_id" instead')
203 return self.project_id
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500204
205 @property
206 def password(self):
207 """The password being used for requests
208
209 :rtype: string
210 :return: The password being used for requests
211 """
212 return self.auth_provider.credentials.password
213
214 @property
215 def base_url(self):
216 return self.auth_provider.base_url(filters=self.filters)
217
218 @property
219 def token(self):
220 return self.auth_provider.get_token()
221
222 @property
223 def filters(self):
224 _filters = dict(
225 service=self.service,
226 endpoint_type=self.endpoint_type,
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -0600227 region=self.region,
228 name=self.name
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500229 )
230 if self.api_version is not None:
231 _filters['api_version'] = self.api_version
232 if self._skip_path:
233 _filters['skip_path'] = self._skip_path
234 return _filters
235
236 def skip_path(self):
237 """When set, ignore the path part of the base URL from the catalog"""
238 self._skip_path = True
239
240 def reset_path(self):
241 """When reset, use the base URL from the catalog as-is"""
242 self._skip_path = False
243
244 @classmethod
245 def expected_success(cls, expected_code, read_code):
246 """Check expected success response code against the http response
247
248 :param int expected_code: The response code that is expected.
249 Optionally a list of integers can be used
250 to specify multiple valid success codes
251 :param int read_code: The response code which was returned in the
252 response
253 :raises AssertionError: if the expected_code isn't a valid http success
254 response code
255 :raises exceptions.InvalidHttpSuccessCode: if the read code isn't an
256 expected http success code
257 """
ghanshyamc3074202016-04-18 15:20:45 +0900258 if not isinstance(read_code, int):
259 raise TypeError("'read_code' must be an int instead of (%s)"
260 % type(read_code))
261
Hanxi2f977db2016-09-01 17:31:28 +0800262 assert_msg = ("This function only allowed to use for HTTP status "
263 "codes which explicitly defined in the RFC 7231 & 4918. "
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500264 "{0} is not a defined Success Code!"
265 ).format(expected_code)
266 if isinstance(expected_code, list):
267 for code in expected_code:
268 assert code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg
269 else:
270 assert expected_code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg
271
272 # NOTE(afazekas): the http status code above 400 is processed by
273 # the _error_checker method
274 if read_code < 400:
zhufl4d2f5152017-01-17 11:16:12 +0800275 pattern = ("Unexpected http success status code {0}, "
276 "The expected status code is {1}")
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500277 if ((not isinstance(expected_code, list) and
278 (read_code != expected_code)) or
279 (isinstance(expected_code, list) and
280 (read_code not in expected_code))):
281 details = pattern.format(read_code, expected_code)
282 raise exceptions.InvalidHttpSuccessCode(details)
283
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200284 def post(self, url, body, headers=None, extra_headers=False,
285 chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500286 """Send a HTTP POST request using keystone auth
287
288 :param str url: the relative url to send the post request to
289 :param dict body: the request body
290 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300291 :param bool extra_headers: Boolean value than indicates if the headers
292 returned by the get_headers() method are to
293 be used but additional headers are needed in
294 the request pass them in as a dict.
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200295 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500296 :return: a tuple with the first entry containing the response headers
297 and the second the response body
298 :rtype: tuple
299 """
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200300 return self.request('POST', url, extra_headers, headers, body, chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500301
302 def get(self, url, headers=None, extra_headers=False):
303 """Send a HTTP GET request using keystone service catalog and auth
304
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530305 :param str url: the relative url to send the get request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500306 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300307 :param bool extra_headers: Boolean value than indicates if the headers
308 returned by the get_headers() method are to
309 be used but additional headers are needed in
310 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500311 :return: a tuple with the first entry containing the response headers
312 and the second the response body
313 :rtype: tuple
314 """
315 return self.request('GET', url, extra_headers, headers)
316
317 def delete(self, url, headers=None, body=None, extra_headers=False):
318 """Send a HTTP DELETE request using keystone service catalog and auth
319
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530320 :param str url: the relative url to send the delete request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500321 :param dict headers: The headers to use for the request
322 :param dict body: the request body
vsaienko9eb846b2016-04-09 00:35:47 +0300323 :param bool extra_headers: Boolean value than indicates if the headers
324 returned by the get_headers() method are to
325 be used but additional headers are needed in
326 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500327 :return: a tuple with the first entry containing the response headers
328 and the second the response body
329 :rtype: tuple
330 """
331 return self.request('DELETE', url, extra_headers, headers, body)
332
333 def patch(self, url, body, headers=None, extra_headers=False):
334 """Send a HTTP PATCH request using keystone service catalog and auth
335
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530336 :param str url: the relative url to send the patch request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500337 :param dict body: the request body
338 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300339 :param bool extra_headers: Boolean value than indicates if the headers
340 returned by the get_headers() method are to
341 be used but additional headers are needed in
342 the request pass them in as a dict.
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 """
347 return self.request('PATCH', url, extra_headers, headers, body)
348
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200349 def put(self, url, body, headers=None, extra_headers=False, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500350 """Send a HTTP PUT 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 put request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500353 :param dict body: the request body
354 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300355 :param bool extra_headers: Boolean value than indicates if the headers
356 returned by the get_headers() method are to
357 be used but additional headers are needed in
358 the request pass them in as a dict.
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200359 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500360 :return: a tuple with the first entry containing the response headers
361 and the second the response body
362 :rtype: tuple
363 """
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200364 return self.request('PUT', url, extra_headers, headers, body, chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500365
366 def head(self, url, headers=None, extra_headers=False):
367 """Send a HTTP HEAD request using keystone service catalog and auth
368
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530369 :param str url: the relative url to send the head request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500370 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300371 :param bool extra_headers: Boolean value than indicates if the headers
372 returned by the get_headers() method are to
373 be used but additional headers are needed in
374 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500375 :return: a tuple with the first entry containing the response headers
376 and the second the response body
377 :rtype: tuple
378 """
379 return self.request('HEAD', url, extra_headers, headers)
380
381 def copy(self, url, headers=None, extra_headers=False):
382 """Send a HTTP COPY request using keystone service catalog and auth
383
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530384 :param str url: the relative url to send the copy request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500385 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300386 :param bool extra_headers: Boolean value than indicates if the headers
387 returned by the get_headers() method are to
388 be used but additional headers are needed in
389 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500390 :return: a tuple with the first entry containing the response headers
391 and the second the response body
392 :rtype: tuple
393 """
394 return self.request('COPY', url, extra_headers, headers)
395
396 def get_versions(self):
sunqingliang699690f62018-11-09 15:03:17 +0800397 """Get the versions on an endpoint from the keystone catalog
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500398
399 This method will make a GET request on the baseurl from the keystone
400 catalog to return a list of API versions. It is expected that a GET
401 on the endpoint in the catalog will return a list of supported API
402 versions.
403
junboli872ca872017-07-21 13:24:38 +0800404 :return: tuple with response headers and list of version numbers
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500405 :rtype: tuple
406 """
407 resp, body = self.get('')
408 body = self._parse_resp(body)
409 versions = map(lambda x: x['id'], body)
410 return resp, versions
411
412 def _get_request_id(self, resp):
413 for i in ('x-openstack-request-id', 'x-compute-request-id'):
414 if i in resp:
415 return resp[i]
416 return ""
417
418 def _safe_body(self, body, maxlen=4096):
419 # convert a structure into a string safely
420 try:
421 text = six.text_type(body)
422 except UnicodeDecodeError:
423 # if this isn't actually text, return marker that
424 return "<BinaryData: removed>"
425 if len(text) > maxlen:
426 return text[:maxlen]
427 else:
428 return text
429
guo yunxian9f749f92016-08-25 10:55:04 +0800430 def _log_request_start(self, method, req_url):
Jordan Pittier9e227c52016-02-09 14:35:18 +0100431 caller_name = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500432 if self.trace_requests and re.search(self.trace_requests, caller_name):
Jordan Pittier525ec712016-12-07 17:51:26 +0100433 self.LOG.debug('Starting Request (%s): %s %s', caller_name,
434 method, req_url)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500435
guo yunxian9f749f92016-08-25 10:55:04 +0800436 def _log_request_full(self, resp, req_headers=None, req_body=None,
437 resp_body=None, extra=None):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500438 if 'X-Auth-Token' in req_headers:
439 req_headers['X-Auth-Token'] = '<omitted>'
Ken'ichi Ohmichi2902a7b2018-07-14 02:31:03 +0000440 if 'X-Subject-Token' in req_headers:
441 req_headers['X-Subject-Token'] = '<omitted>'
Andrea Frittoli (andreaf)a1edb2d2016-05-10 16:09:59 +0100442 # A shallow copy is sufficient
443 resp_log = resp.copy()
444 if 'x-subject-token' in resp_log:
445 resp_log['x-subject-token'] = '<omitted>'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500446 log_fmt = """Request - Headers: %s
447 Body: %s
448 Response - Headers: %s
449 Body: %s"""
450
451 self.LOG.debug(
Jordan Pittier525ec712016-12-07 17:51:26 +0100452 log_fmt,
453 str(req_headers),
454 self._safe_body(req_body),
455 str(resp_log),
456 self._safe_body(resp_body),
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500457 extra=extra)
458
459 def _log_request(self, method, req_url, resp,
460 secs="", req_headers=None,
461 req_body=None, resp_body=None):
462 if req_headers is None:
463 req_headers = {}
464 # if we have the request id, put it in the right part of the log
465 extra = dict(request_id=self._get_request_id(resp))
466 # NOTE(sdague): while we still have 6 callers to this function
467 # we're going to just provide work around on who is actually
468 # providing timings by gracefully adding no content if they don't.
469 # Once we're down to 1 caller, clean this up.
Jordan Pittier9e227c52016-02-09 14:35:18 +0100470 caller_name = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500471 if secs:
472 secs = " %.3fs" % secs
473 self.LOG.info(
Jordan Pittier525ec712016-12-07 17:51:26 +0100474 'Request (%s): %s %s %s%s',
475 caller_name,
476 resp['status'],
477 method,
478 req_url,
479 secs,
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500480 extra=extra)
481
482 # Also look everything at DEBUG if you want to filter this
483 # out, don't run at debug.
Anusha Raminenif3eb9472017-01-13 08:54:01 +0530484 if self.LOG.isEnabledFor(logging.DEBUG):
guo yunxian9f749f92016-08-25 10:55:04 +0800485 self._log_request_full(resp, req_headers, req_body,
486 resp_body, extra)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500487
488 def _parse_resp(self, body):
489 try:
490 body = json.loads(body)
491 except ValueError:
492 return body
493
494 # We assume, that if the first value of the deserialized body's
495 # item set is a dict or a list, that we just return the first value
496 # of deserialized body.
497 # Essentially "cutting out" the first placeholder element in a body
498 # that looks like this:
499 #
500 # {
501 # "users": [
502 # ...
503 # ]
504 # }
505 try:
506 # Ensure there are not more than one top-level keys
507 # NOTE(freerunner): Ensure, that JSON is not nullable to
508 # to prevent StopIteration Exception
Ken'ichi Ohmichi69a8edc2017-04-28 11:41:20 -0700509 if not hasattr(body, "keys") or len(body.keys()) != 1:
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500510 return body
511 # Just return the "wrapped" element
zhufl3ead9982020-11-19 14:39:04 +0800512 _, first_item = six.next(six.iteritems(body))
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500513 if isinstance(first_item, (dict, list)):
514 return first_item
515 except (ValueError, IndexError):
516 pass
517 return body
518
519 def response_checker(self, method, resp, resp_body):
520 """A sanity check on the response from a HTTP request
521
522 This method does a sanity check on whether the response from an HTTP
523 request conforms the HTTP RFC.
524
525 :param str method: The HTTP verb of the request associated with the
526 response being passed in.
527 :param resp: The response headers
528 :param resp_body: The body of the response
529 :raises ResponseWithNonEmptyBody: If the response with the status code
530 is not supposed to have a body
531 :raises ResponseWithEntity: If the response code is 205 but has an
532 entity
533 """
534 if (resp.status in set((204, 205, 304)) or resp.status < 200 or
535 method.upper() == 'HEAD') and resp_body:
536 raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
537 # NOTE(afazekas):
538 # If the HTTP Status Code is 205
539 # 'The response MUST NOT include an entity.'
540 # A HTTP entity has an entity-body and an 'entity-header'.
541 # In the HTTP response specification (Section 6) the 'entity-header'
542 # 'generic-header' and 'response-header' are in OR relation.
543 # All headers not in the above two group are considered as entity
544 # header in every interpretation.
545
546 if (resp.status == 205 and
547 0 != len(set(resp.keys()) - set(('status',)) -
548 self.response_header_lc - self.general_header_lc)):
Matt Riedemann91d92422019-01-29 16:19:49 -0500549 raise exceptions.ResponseWithEntity()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500550 # NOTE(afazekas)
551 # Now the swift sometimes (delete not empty container)
552 # returns with non json error response, we can create new rest class
553 # for swift.
554 # Usually RFC2616 says error responses SHOULD contain an explanation.
555 # The warning is normal for SHOULD/SHOULD NOT case
556
557 # Likely it will cause an error
558 if method != 'HEAD' and not resp_body and resp.status >= 400:
559 self.LOG.warning("status >= 400 response with empty body")
560
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200561 def _request(self, method, url, headers=None, body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500562 """A simple HTTP request interface."""
563 # Authenticate the request with the auth provider
564 req_url, req_headers, req_body = self.auth_provider.auth_request(
565 method, url, headers, body, self.filters)
566
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500567 resp, resp_body = self.raw_request(
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200568 req_url, method, headers=req_headers, body=req_body,
569 chunked=chunked
570 )
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500571 # Verify HTTP response codes
572 self.response_checker(method, resp, resp_body)
573
574 return resp, resp_body
575
Ghanshyam Mann47a41992019-10-02 16:56:26 +0000576 def raw_request(self, url, method, headers=None, body=None, chunked=False,
577 log_req_body=None):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500578 """Send a raw HTTP request without the keystone catalog or auth
579
580 This method sends a HTTP request in the same manner as the request()
581 method, however it does so without using keystone auth or the catalog
582 to determine the base url. Additionally no response handling is done
583 the results from the request are just returned.
584
585 :param str url: Full url to send the request
586 :param str method: The HTTP verb to use for the request
zhuflcf35f652018-08-17 10:13:43 +0800587 :param dict headers: Headers to use for the request. If none are
588 specified, then the headers returned from the
589 get_headers() method are used. If the request
590 explicitly requires no headers use an empty dict.
Anh Trand44a8be2016-03-25 09:49:14 +0700591 :param str body: Body to send with the request
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200592 :param bool chunked: sends the body with chunked encoding
Ghanshyam Mann47a41992019-10-02 16:56:26 +0000593 :param str log_req_body: Whether to log the request body or not.
594 It is default to None which means request
595 body is safe to log otherwise pass any string
596 you want to log in place of request body.
597 For example: '<omitted>'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500598 :rtype: tuple
599 :return: a tuple with the first entry containing the response headers
600 and the second the response body
601 """
602 if headers is None:
603 headers = self.get_headers()
Ghanshyam Manncb3cf032019-09-26 00:02:54 +0000604 # Do the actual request, and time it
605 start = time.time()
606 self._log_request_start(method, url)
607 resp, resp_body = self.http_obj.request(
608 url, method, headers=headers,
609 body=body, chunked=chunked)
610 end = time.time()
Ghanshyam Mann47a41992019-10-02 16:56:26 +0000611 req_body = body if log_req_body is None else log_req_body
Ghanshyam Manncb3cf032019-09-26 00:02:54 +0000612 self._log_request(method, url, resp, secs=(end - start),
Ghanshyam Mann47a41992019-10-02 16:56:26 +0000613 req_headers=headers, req_body=req_body,
Ghanshyam Manncb3cf032019-09-26 00:02:54 +0000614 resp_body=resp_body)
615 return resp, resp_body
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500616
617 def request(self, method, url, extra_headers=False, headers=None,
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200618 body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500619 """Send a HTTP request with keystone auth and using the catalog
620
621 This method will send an HTTP request using keystone auth in the
622 headers and the catalog to determine the endpoint to use for the
623 baseurl to send the request to. Additionally
624
625 When a response is received it will check it to see if an error
626 response was received. If it was an exception will be raised to enable
627 it to be handled quickly.
628
629 This method will also handle rate-limiting, if a 413 response code is
630 received it will retry the request after waiting the 'retry-after'
631 duration from the header.
632
633 :param str method: The HTTP verb to use for the request
634 :param str url: Relative url to send the request to
vsaienko9eb846b2016-04-09 00:35:47 +0300635 :param bool extra_headers: Boolean value than indicates if the headers
636 returned by the get_headers() method are to
637 be used but additional headers are needed in
638 the request pass them in as a dict.
zhuflcf35f652018-08-17 10:13:43 +0800639 :param dict headers: Headers to use for the request. If none are
640 specified, then the headers returned from the
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500641 get_headers() method are used. If the request
642 explicitly requires no headers use an empty dict.
Anh Trand44a8be2016-03-25 09:49:14 +0700643 :param str body: Body to send with the request
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200644 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500645 :rtype: tuple
646 :return: a tuple with the first entry containing the response headers
647 and the second the response body
648 :raises UnexpectedContentType: If the content-type of the response
649 isn't an expect type
650 :raises Unauthorized: If a 401 response code is received
651 :raises Forbidden: If a 403 response code is received
652 :raises NotFound: If a 404 response code is received
653 :raises BadRequest: If a 400 response code is received
654 :raises Gone: If a 410 response code is received
655 :raises Conflict: If a 409 response code is received
Kevin Bentona82bc862017-02-13 01:16:13 -0800656 :raises PreconditionFailed: If a 412 response code is received
zhuflcf35f652018-08-17 10:13:43 +0800657 :raises OverLimit: If a 413 response code is received and retry-after
658 is not in the response body or its retry operation
659 exceeds the limits defined by the server
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500660 :raises RateLimitExceeded: If a 413 response code is received and
zhuflcf35f652018-08-17 10:13:43 +0800661 retry-after is in the response body and
662 its retry operation does not exceeds the
663 limits defined by the server
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500664 :raises InvalidContentType: If a 415 response code is received
665 :raises UnprocessableEntity: If a 422 response code is received
666 :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
667 and couldn't be parsed
668 :raises NotImplemented: If a 501 response code is received
669 :raises ServerFault: If a 500 response code is received
670 :raises UnexpectedResponseCode: If a response code above 400 is
671 received and it doesn't fall into any
672 of the handled checks
673 """
674 # if extra_headers is True
675 # default headers would be added to headers
676 retry = 0
677
678 if headers is None:
679 # NOTE(vponomaryov): if some client do not need headers,
680 # it should explicitly pass empty dict
681 headers = self.get_headers()
682 elif extra_headers:
683 try:
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500684 headers.update(self.get_headers())
685 except (ValueError, TypeError):
686 headers = self.get_headers()
687
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200688 resp, resp_body = self._request(method, url, headers=headers,
689 body=body, chunked=chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500690
691 while (resp.status == 413 and
692 'retry-after' in resp and
693 not self.is_absolute_limit(
694 resp, self._parse_resp(resp_body)) and
695 retry < MAX_RECURSION_DEPTH):
696 retry += 1
Paul Glass119565a2016-04-06 11:41:42 -0500697 delay = self._get_retry_after_delay(resp)
698 self.LOG.debug(
699 "Sleeping %s seconds based on retry-after header", delay
700 )
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500701 time.sleep(delay)
702 resp, resp_body = self._request(method, url,
703 headers=headers, body=body)
Ken'ichi Ohmichi17051e82016-10-20 11:46:06 -0700704 self._error_checker(resp, resp_body)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500705 return resp, resp_body
706
Paul Glass119565a2016-04-06 11:41:42 -0500707 def _get_retry_after_delay(self, resp):
708 """Extract the delay from the retry-after header.
709
710 This supports both integer and HTTP date formatted retry-after headers
711 per RFC 2616.
712
713 :param resp: The response containing the retry-after headers
714 :rtype: int
715 :return: The delay in seconds, clamped to be at least 1 second
716 :raises ValueError: On failing to parse the delay
717 """
718 delay = None
719 try:
720 delay = int(resp['retry-after'])
721 except (ValueError, KeyError):
722 pass
723
724 try:
725 retry_timestamp = self._parse_http_date(resp['retry-after'])
726 date_timestamp = self._parse_http_date(resp['date'])
727 delay = int(retry_timestamp - date_timestamp)
728 except (ValueError, OverflowError, KeyError):
729 pass
730
731 if delay is None:
732 raise ValueError(
733 "Failed to parse retry-after header %r as either int or "
734 "HTTP-date." % resp.get('retry-after')
735 )
736
737 # Retry-after headers do not have sub-second precision. Clients may
738 # receive a delay of 0. After sleeping 0 seconds, we would (likely) hit
739 # another 413. To avoid this, always sleep at least 1 second.
740 return max(1, delay)
741
742 def _parse_http_date(self, val):
743 """Parse an HTTP date, like 'Fri, 31 Dec 1999 23:59:59 GMT'.
744
745 Return an epoch timestamp (float), as returned by time.mktime().
746 """
747 parts = email.utils.parsedate(val)
748 if not parts:
749 raise ValueError("Failed to parse date %s" % val)
750 return time.mktime(parts)
751
Ken'ichi Ohmichi17051e82016-10-20 11:46:06 -0700752 def _error_checker(self, resp, resp_body):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500753
754 # NOTE(mtreinish): Check for httplib response from glance_http. The
755 # object can't be used here because importing httplib breaks httplib2.
756 # If another object from a class not imported were passed here as
757 # resp this could possibly fail
758 if str(type(resp)) == "<type 'instance'>":
759 ctype = resp.getheader('content-type')
760 else:
761 try:
762 ctype = resp['content-type']
763 # NOTE(mtreinish): Keystone delete user responses doesn't have a
764 # content-type header. (They don't have a body) So just pretend it
765 # is set.
766 except KeyError:
767 ctype = 'application/json'
768
769 # It is not an error response
770 if resp.status < 400:
771 return
772
zhipenghd1db0c72017-02-21 04:40:07 -0500773 # NOTE(zhipengh): There is a purposefully duplicate of content-type
774 # with the only difference is with or without spaces, as specified
775 # in RFC7231.
776 JSON_ENC = ['application/json', 'application/json; charset=utf-8',
777 'application/json;charset=utf-8']
778
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500779 # NOTE(mtreinish): This is for compatibility with Glance and swift
780 # APIs. These are the return content types that Glance api v1
781 # (and occasionally swift) are using.
zhipenghd1db0c72017-02-21 04:40:07 -0500782 # NOTE(zhipengh): There is a purposefully duplicate of content-type
783 # with the only difference is with or without spaces, as specified
784 # in RFC7231.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500785 TXT_ENC = ['text/plain', 'text/html', 'text/html; charset=utf-8',
zhipenghd1db0c72017-02-21 04:40:07 -0500786 'text/plain; charset=utf-8', 'text/html;charset=utf-8',
787 'text/plain;charset=utf-8']
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500788
789 if ctype.lower() in JSON_ENC:
790 parse_resp = True
791 elif ctype.lower() in TXT_ENC:
792 parse_resp = False
793 else:
794 raise exceptions.UnexpectedContentType(str(resp.status),
795 resp=resp)
796
797 if resp.status == 401:
798 if parse_resp:
799 resp_body = self._parse_resp(resp_body)
800 raise exceptions.Unauthorized(resp_body, resp=resp)
801
802 if resp.status == 403:
803 if parse_resp:
804 resp_body = self._parse_resp(resp_body)
805 raise exceptions.Forbidden(resp_body, resp=resp)
806
807 if resp.status == 404:
808 if parse_resp:
809 resp_body = self._parse_resp(resp_body)
810 raise exceptions.NotFound(resp_body, resp=resp)
811
812 if resp.status == 400:
813 if parse_resp:
814 resp_body = self._parse_resp(resp_body)
815 raise exceptions.BadRequest(resp_body, resp=resp)
816
817 if resp.status == 410:
818 if parse_resp:
819 resp_body = self._parse_resp(resp_body)
820 raise exceptions.Gone(resp_body, resp=resp)
821
822 if resp.status == 409:
823 if parse_resp:
824 resp_body = self._parse_resp(resp_body)
825 raise exceptions.Conflict(resp_body, resp=resp)
826
Kevin Bentona82bc862017-02-13 01:16:13 -0800827 if resp.status == 412:
828 if parse_resp:
829 resp_body = self._parse_resp(resp_body)
830 raise exceptions.PreconditionFailed(resp_body, resp=resp)
831
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500832 if resp.status == 413:
833 if parse_resp:
834 resp_body = self._parse_resp(resp_body)
835 if self.is_absolute_limit(resp, resp_body):
836 raise exceptions.OverLimit(resp_body, resp=resp)
837 else:
838 raise exceptions.RateLimitExceeded(resp_body, resp=resp)
839
840 if resp.status == 415:
841 if parse_resp:
842 resp_body = self._parse_resp(resp_body)
843 raise exceptions.InvalidContentType(resp_body, resp=resp)
844
845 if resp.status == 422:
846 if parse_resp:
847 resp_body = self._parse_resp(resp_body)
848 raise exceptions.UnprocessableEntity(resp_body, resp=resp)
849
850 if resp.status in (500, 501):
851 message = resp_body
852 if parse_resp:
853 try:
854 resp_body = self._parse_resp(resp_body)
855 except ValueError:
856 # If response body is a non-json string message.
857 # Use resp_body as is and raise InvalidResponseBody
858 # exception.
859 raise exceptions.InvalidHTTPResponseBody(message)
860 else:
861 if isinstance(resp_body, dict):
862 # I'm seeing both computeFault
863 # and cloudServersFault come back.
864 # Will file a bug to fix, but leave as is for now.
865 if 'cloudServersFault' in resp_body:
866 message = resp_body['cloudServersFault']['message']
867 elif 'computeFault' in resp_body:
868 message = resp_body['computeFault']['message']
869 elif 'error' in resp_body:
870 message = resp_body['error']['message']
871 elif 'message' in resp_body:
872 message = resp_body['message']
873 else:
874 message = resp_body
875
876 if resp.status == 501:
877 raise exceptions.NotImplemented(resp_body, resp=resp,
878 message=message)
879 else:
880 raise exceptions.ServerFault(resp_body, resp=resp,
881 message=message)
882
883 if resp.status >= 400:
884 raise exceptions.UnexpectedResponseCode(str(resp.status),
885 resp=resp)
886
887 def is_absolute_limit(self, resp, resp_body):
888 if (not isinstance(resp_body, collections.Mapping) or
889 'retry-after' not in resp):
890 return True
Paul Glass119565a2016-04-06 11:41:42 -0500891 return 'exceed' in resp_body.get('message', 'blabla')
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500892
893 def wait_for_resource_deletion(self, id):
894 """Waits for a resource to be deleted
895
896 This method will loop over is_resource_deleted until either
897 is_resource_deleted returns True or the build timeout is reached. This
898 depends on is_resource_deleted being implemented
899
900 :param str id: The id of the resource to check
901 :raises TimeoutException: If the build_timeout has elapsed and the
902 resource still hasn't been deleted
903 """
904 start_time = int(time.time())
905 while True:
906 if self.is_resource_deleted(id):
907 return
908 if int(time.time()) - start_time >= self.build_timeout:
909 message = ('Failed to delete %(resource_type)s %(id)s within '
910 'the required time (%(timeout)s s).' %
911 {'resource_type': self.resource_type, 'id': id,
912 'timeout': self.build_timeout})
Jordan Pittier9e227c52016-02-09 14:35:18 +0100913 caller = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500914 if caller:
915 message = '(%s) %s' % (caller, message)
916 raise exceptions.TimeoutException(message)
917 time.sleep(self.build_interval)
918
Abhishek Kekane7cff1302020-07-16 10:30:13 +0000919 def wait_for_resource_activation(self, id):
920 """Waits for a resource to become active
921
922 This method will loop over is_resource_active until either
923 is_resource_active returns True or the build timeout is reached. This
924 depends on is_resource_active being implemented
925
926 :param str id: The id of the resource to check
927 :raises TimeoutException: If the build_timeout has elapsed and the
928 resource still hasn't been active
929 """
930 start_time = int(time.time())
931 while True:
932 if self.is_resource_active(id):
933 return
934 if int(time.time()) - start_time >= self.build_timeout:
935 message = ('Failed to reach active state %(resource_type)s '
936 '%(id)s within the required time (%(timeout)s s).' %
937 {'resource_type': self.resource_type, 'id': id,
938 'timeout': self.build_timeout})
939 caller = test_utils.find_test_caller()
940 if caller:
941 message = '(%s) %s' % (caller, message)
942 raise exceptions.TimeoutException(message)
943 time.sleep(self.build_interval)
944
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500945 def is_resource_deleted(self, id):
946 """Subclasses override with specific deletion detection."""
947 message = ('"%s" does not implement is_resource_deleted'
948 % self.__class__.__name__)
949 raise NotImplementedError(message)
950
Abhishek Kekane7cff1302020-07-16 10:30:13 +0000951 def is_resource_active(self, id):
952 """Subclasses override with specific active detection."""
953 message = ('"%s" does not implement is_resource_active'
954 % self.__class__.__name__)
955 raise NotImplementedError(message)
956
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500957 @property
958 def resource_type(self):
959 """Returns the primary type of resource this client works with."""
960 return 'resource'
961
962 @classmethod
963 def validate_response(cls, schema, resp, body):
964 # Only check the response if the status code is a success code
965 # TODO(cyeoh): Eventually we should be able to verify that a failure
966 # code if it exists is something that we expect. This is explicitly
967 # declared in the V3 API and so we should be able to export this in
968 # the response schema. For now we'll ignore it.
969 if resp.status in HTTP_SUCCESS + HTTP_REDIRECTION:
970 cls.expected_success(schema['status_code'], resp.status)
971
972 # Check the body of a response
973 body_schema = schema.get('response_body')
974 if body_schema:
975 try:
976 jsonschema.validate(body, body_schema,
977 cls=JSONSCHEMA_VALIDATOR,
978 format_checker=FORMAT_CHECKER)
979 except jsonschema.ValidationError as ex:
guo yunxiana3f55282016-08-10 14:35:16 +0800980 msg = ("HTTP response body is invalid (%s)" % ex)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500981 raise exceptions.InvalidHTTPResponseBody(msg)
982 else:
983 if body:
guo yunxiana3f55282016-08-10 14:35:16 +0800984 msg = ("HTTP response body should not exist (%s)" % body)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500985 raise exceptions.InvalidHTTPResponseBody(msg)
986
987 # Check the header of a response
988 header_schema = schema.get('response_header')
989 if header_schema:
990 try:
991 jsonschema.validate(resp, header_schema,
992 cls=JSONSCHEMA_VALIDATOR,
993 format_checker=FORMAT_CHECKER)
994 except jsonschema.ValidationError as ex:
guo yunxiana3f55282016-08-10 14:35:16 +0800995 msg = ("HTTP response header is invalid (%s)" % ex)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500996 raise exceptions.InvalidHTTPResponseHeader(msg)
997
Ken'ichi Ohmichida26b162017-03-03 15:53:46 -0800998 def _get_base_version_url(self):
999 # TODO(oomichi): This method can be used for auth's replace_version().
1000 # So it is nice to have common logic for the maintenance.
1001 endpoint = self.base_url
1002 url = urllib.parse.urlsplit(endpoint)
1003 new_path = re.split(r'(^|/)+v\d+(\.\d+)?', url.path)[0]
1004 url = list(url)
1005 url[2] = new_path + '/'
1006 return urllib.parse.urlunsplit(url)
1007
Matthew Treinish9e26ca82016-02-23 11:43:20 -05001008
1009class ResponseBody(dict):
1010 """Class that wraps an http response and dict body into a single value.
1011
1012 Callers that receive this object will normally use it as a dict but
1013 can extract the response if needed.
1014 """
1015
1016 def __init__(self, response, body=None):
1017 body_data = body or {}
1018 self.update(body_data)
1019 self.response = response
1020
1021 def __str__(self):
1022 body = super(ResponseBody, self).__str__()
1023 return "response: %s\nBody: %s" % (self.response, body)
1024
1025
1026class ResponseBodyData(object):
1027 """Class that wraps an http response and string data into a single value.
1028
1029 """
1030
1031 def __init__(self, response, data):
1032 self.response = response
1033 self.data = data
1034
1035 def __str__(self):
1036 return "response: %s\nBody: %s" % (self.response, self.data)
1037
1038
1039class ResponseBodyList(list):
1040 """Class that wraps an http response and list body into a single value.
1041
1042 Callers that receive this object will normally use it as a list but
1043 can extract the response if needed.
1044 """
1045
1046 def __init__(self, response, body=None):
1047 body_data = body or []
1048 self.extend(body_data)
1049 self.response = response
1050
1051 def __str__(self):
1052 body = super(ResponseBodyList, self).__str__()
1053 return "response: %s\nBody: %s" % (self.response, body)