blob: a2f2931b345c40c05c6cc2fd5e5a0c37316dd449 [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
Takashi Kajinami6354f612021-07-17 00:37:34 +090017from collections import abc
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
Dan Smith2c192f42023-01-18 11:22:34 -080022import urllib3
Matthew Treinish9e26ca82016-02-23 11:43:20 -050023
24import jsonschema
25from oslo_log import log as logging
Rodolfo Alonso Hernandezc1449d42020-02-15 13:24:28 +000026from oslo_log import versionutils
Matthew Treinish9e26ca82016-02-23 11:43:20 -050027from oslo_serialization import jsonutils as json
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'))
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
Dan Smith2c192f42023-01-18 11:22:34 -0800302 def get(self, url, headers=None, extra_headers=False, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500303 """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.
Dan Smith2c192f42023-01-18 11:22:34 -0800311 :param bool chunked: Boolean value that indicates if we should stream
312 the response instead of reading it all at once.
313 If True, data will be empty and the raw urllib3
314 response object will be returned.
315 NB: If you pass True here, you **MUST** call
316 release_conn() on the response object before
317 finishing!
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500318 :return: a tuple with the first entry containing the response headers
319 and the second the response body
320 :rtype: tuple
321 """
Dan Smith2c192f42023-01-18 11:22:34 -0800322 return self.request('GET', url, extra_headers, headers,
323 chunked=chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500324
325 def delete(self, url, headers=None, body=None, extra_headers=False):
326 """Send a HTTP DELETE request using keystone service catalog and auth
327
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530328 :param str url: the relative url to send the delete request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500329 :param dict headers: The headers to use for the request
330 :param dict body: the request body
vsaienko9eb846b2016-04-09 00:35:47 +0300331 :param bool extra_headers: Boolean value than indicates if the headers
332 returned by the get_headers() method are to
333 be used but additional headers are needed in
334 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500335 :return: a tuple with the first entry containing the response headers
336 and the second the response body
337 :rtype: tuple
338 """
339 return self.request('DELETE', url, extra_headers, headers, body)
340
341 def patch(self, url, body, headers=None, extra_headers=False):
342 """Send a HTTP PATCH request using keystone service catalog and auth
343
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530344 :param str url: the relative url to send the patch request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500345 :param dict body: the request body
346 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300347 :param bool extra_headers: Boolean value than indicates if the headers
348 returned by the get_headers() method are to
349 be used but additional headers are needed in
350 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500351 :return: a tuple with the first entry containing the response headers
352 and the second the response body
353 :rtype: tuple
354 """
355 return self.request('PATCH', url, extra_headers, headers, body)
356
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200357 def put(self, url, body, headers=None, extra_headers=False, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500358 """Send a HTTP PUT request using keystone service catalog and auth
359
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530360 :param str url: the relative url to send the put request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500361 :param dict body: the request body
362 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300363 :param bool extra_headers: Boolean value than indicates if the headers
364 returned by the get_headers() method are to
365 be used but additional headers are needed in
366 the request pass them in as a dict.
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200367 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500368 :return: a tuple with the first entry containing the response headers
369 and the second the response body
370 :rtype: tuple
371 """
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200372 return self.request('PUT', url, extra_headers, headers, body, chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500373
374 def head(self, url, headers=None, extra_headers=False):
375 """Send a HTTP HEAD request using keystone service catalog and auth
376
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530377 :param str url: the relative url to send the head request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500378 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300379 :param bool extra_headers: Boolean value than indicates if the headers
380 returned by the get_headers() method are to
381 be used but additional headers are needed in
382 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500383 :return: a tuple with the first entry containing the response headers
384 and the second the response body
385 :rtype: tuple
386 """
387 return self.request('HEAD', url, extra_headers, headers)
388
389 def copy(self, url, headers=None, extra_headers=False):
390 """Send a HTTP COPY request using keystone service catalog and auth
391
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530392 :param str url: the relative url to send the copy request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500393 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300394 :param bool extra_headers: Boolean value than indicates if the headers
395 returned by the get_headers() method are to
396 be used but additional headers are needed in
397 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500398 :return: a tuple with the first entry containing the response headers
399 and the second the response body
400 :rtype: tuple
401 """
402 return self.request('COPY', url, extra_headers, headers)
403
404 def get_versions(self):
sunqingliang699690f62018-11-09 15:03:17 +0800405 """Get the versions on an endpoint from the keystone catalog
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500406
407 This method will make a GET request on the baseurl from the keystone
408 catalog to return a list of API versions. It is expected that a GET
409 on the endpoint in the catalog will return a list of supported API
410 versions.
411
junboli872ca872017-07-21 13:24:38 +0800412 :return: tuple with response headers and list of version numbers
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500413 :rtype: tuple
414 """
415 resp, body = self.get('')
416 body = self._parse_resp(body)
417 versions = map(lambda x: x['id'], body)
418 return resp, versions
419
420 def _get_request_id(self, resp):
421 for i in ('x-openstack-request-id', 'x-compute-request-id'):
422 if i in resp:
423 return resp[i]
424 return ""
425
Stephen Finucane8aa5f892022-05-10 18:20:52 +0100426 def _get_global_request_id(self, resp):
427 if 'x-openstack-request-id' in resp:
428 return resp['x-openstack-request-id']
429 return ''
430
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500431 def _safe_body(self, body, maxlen=4096):
432 # convert a structure into a string safely
433 try:
likui19b70a32020-12-02 13:25:18 +0800434 text = str(body)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500435 except UnicodeDecodeError:
436 # if this isn't actually text, return marker that
437 return "<BinaryData: removed>"
438 if len(text) > maxlen:
439 return text[:maxlen]
440 else:
441 return text
442
guo yunxian9f749f92016-08-25 10:55:04 +0800443 def _log_request_start(self, method, req_url):
Jordan Pittier9e227c52016-02-09 14:35:18 +0100444 caller_name = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500445 if self.trace_requests and re.search(self.trace_requests, caller_name):
Jordan Pittier525ec712016-12-07 17:51:26 +0100446 self.LOG.debug('Starting Request (%s): %s %s', caller_name,
447 method, req_url)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500448
guo yunxian9f749f92016-08-25 10:55:04 +0800449 def _log_request_full(self, resp, req_headers=None, req_body=None,
450 resp_body=None, extra=None):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500451 if 'X-Auth-Token' in req_headers:
452 req_headers['X-Auth-Token'] = '<omitted>'
Ken'ichi Ohmichi2902a7b2018-07-14 02:31:03 +0000453 if 'X-Subject-Token' in req_headers:
454 req_headers['X-Subject-Token'] = '<omitted>'
Andrea Frittoli (andreaf)a1edb2d2016-05-10 16:09:59 +0100455 # A shallow copy is sufficient
456 resp_log = resp.copy()
457 if 'x-subject-token' in resp_log:
458 resp_log['x-subject-token'] = '<omitted>'
Ilya Menkov35ffdfc2021-05-28 19:10:24 +0400459 # NOTE(sdague): while we still have 6 callers to this function
460 # we're going to just provide work around on who is actually
461 # providing timings by gracefully adding no content if they don't.
462 # Once we're down to 1 caller, clean this up.
463 caller_name = test_utils.find_test_caller()
464
465 log_fmt = """Request (%s) - Headers: %s
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500466 Body: %s
467 Response - Headers: %s
468 Body: %s"""
469
470 self.LOG.debug(
Jordan Pittier525ec712016-12-07 17:51:26 +0100471 log_fmt,
Ilya Menkov35ffdfc2021-05-28 19:10:24 +0400472 caller_name,
Jordan Pittier525ec712016-12-07 17:51:26 +0100473 str(req_headers),
474 self._safe_body(req_body),
475 str(resp_log),
476 self._safe_body(resp_body),
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500477 extra=extra)
478
479 def _log_request(self, method, req_url, resp,
480 secs="", req_headers=None,
481 req_body=None, resp_body=None):
482 if req_headers is None:
483 req_headers = {}
484 # if we have the request id, put it in the right part of the log
Stephen Finucane8aa5f892022-05-10 18:20:52 +0100485 extra = {
486 'request_id': self._get_request_id(resp),
487 'global_request_id': self._get_global_request_id(resp),
488 }
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500489 # NOTE(sdague): while we still have 6 callers to this function
490 # we're going to just provide work around on who is actually
491 # providing timings by gracefully adding no content if they don't.
492 # Once we're down to 1 caller, clean this up.
Jordan Pittier9e227c52016-02-09 14:35:18 +0100493 caller_name = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500494 if secs:
495 secs = " %.3fs" % secs
496 self.LOG.info(
Jordan Pittier525ec712016-12-07 17:51:26 +0100497 'Request (%s): %s %s %s%s',
498 caller_name,
Dan Smith2c192f42023-01-18 11:22:34 -0800499 resp.status,
Jordan Pittier525ec712016-12-07 17:51:26 +0100500 method,
501 req_url,
502 secs,
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500503 extra=extra)
504
505 # Also look everything at DEBUG if you want to filter this
506 # out, don't run at debug.
Anusha Raminenif3eb9472017-01-13 08:54:01 +0530507 if self.LOG.isEnabledFor(logging.DEBUG):
guo yunxian9f749f92016-08-25 10:55:04 +0800508 self._log_request_full(resp, req_headers, req_body,
509 resp_body, extra)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500510
Goutham Pacha Ravic0a15ba2022-04-06 23:41:57 +0530511 def _parse_resp(self, body, top_key_to_verify=None):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500512 try:
513 body = json.loads(body)
514 except ValueError:
515 return body
516
517 # We assume, that if the first value of the deserialized body's
518 # item set is a dict or a list, that we just return the first value
519 # of deserialized body.
520 # Essentially "cutting out" the first placeholder element in a body
521 # that looks like this:
522 #
523 # {
524 # "users": [
525 # ...
526 # ]
527 # }
528 try:
529 # Ensure there are not more than one top-level keys
530 # NOTE(freerunner): Ensure, that JSON is not nullable to
531 # to prevent StopIteration Exception
Ken'ichi Ohmichi69a8edc2017-04-28 11:41:20 -0700532 if not hasattr(body, "keys") or len(body.keys()) != 1:
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500533 return body
534 # Just return the "wrapped" element
Goutham Pacha Ravic0a15ba2022-04-06 23:41:57 +0530535 first_key, first_item = tuple(body.items())[0]
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500536 if isinstance(first_item, (dict, list)):
Goutham Pacha Ravic0a15ba2022-04-06 23:41:57 +0530537 if top_key_to_verify is not None:
538 msg_args = {
539 'top_key': top_key_to_verify,
540 'actual_key': first_key,
541 }
542 assert_msg = ("The expected top level key is "
543 "'%(top_key)s' but we found "
544 "'%(actual_key)s'." % msg_args)
545 assert top_key_to_verify == first_key, assert_msg
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500546 return first_item
547 except (ValueError, IndexError):
548 pass
549 return body
550
551 def response_checker(self, method, resp, resp_body):
552 """A sanity check on the response from a HTTP request
553
554 This method does a sanity check on whether the response from an HTTP
555 request conforms the HTTP RFC.
556
557 :param str method: The HTTP verb of the request associated with the
558 response being passed in.
559 :param resp: The response headers
560 :param resp_body: The body of the response
561 :raises ResponseWithNonEmptyBody: If the response with the status code
562 is not supposed to have a body
563 :raises ResponseWithEntity: If the response code is 205 but has an
564 entity
565 """
566 if (resp.status in set((204, 205, 304)) or resp.status < 200 or
567 method.upper() == 'HEAD') and resp_body:
568 raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
569 # NOTE(afazekas):
570 # If the HTTP Status Code is 205
571 # 'The response MUST NOT include an entity.'
572 # A HTTP entity has an entity-body and an 'entity-header'.
573 # In the HTTP response specification (Section 6) the 'entity-header'
574 # 'generic-header' and 'response-header' are in OR relation.
575 # All headers not in the above two group are considered as entity
576 # header in every interpretation.
577
578 if (resp.status == 205 and
579 0 != len(set(resp.keys()) - set(('status',)) -
580 self.response_header_lc - self.general_header_lc)):
Matt Riedemann91d92422019-01-29 16:19:49 -0500581 raise exceptions.ResponseWithEntity()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500582 # NOTE(afazekas)
583 # Now the swift sometimes (delete not empty container)
584 # returns with non json error response, we can create new rest class
585 # for swift.
586 # Usually RFC2616 says error responses SHOULD contain an explanation.
587 # The warning is normal for SHOULD/SHOULD NOT case
588
589 # Likely it will cause an error
590 if method != 'HEAD' and not resp_body and resp.status >= 400:
591 self.LOG.warning("status >= 400 response with empty body")
592
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200593 def _request(self, method, url, headers=None, body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500594 """A simple HTTP request interface."""
595 # Authenticate the request with the auth provider
596 req_url, req_headers, req_body = self.auth_provider.auth_request(
597 method, url, headers, body, self.filters)
598
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500599 resp, resp_body = self.raw_request(
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200600 req_url, method, headers=req_headers, body=req_body,
601 chunked=chunked
602 )
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500603 # Verify HTTP response codes
604 self.response_checker(method, resp, resp_body)
605
606 return resp, resp_body
607
Ghanshyam Mann47a41992019-10-02 16:56:26 +0000608 def raw_request(self, url, method, headers=None, body=None, chunked=False,
609 log_req_body=None):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500610 """Send a raw HTTP request without the keystone catalog or auth
611
612 This method sends a HTTP request in the same manner as the request()
613 method, however it does so without using keystone auth or the catalog
614 to determine the base url. Additionally no response handling is done
615 the results from the request are just returned.
616
617 :param str url: Full url to send the request
618 :param str method: The HTTP verb to use for the request
zhuflcf35f652018-08-17 10:13:43 +0800619 :param dict headers: Headers to use for the request. If none are
620 specified, then the headers returned from the
621 get_headers() method are used. If the request
622 explicitly requires no headers use an empty dict.
Anh Trand44a8be2016-03-25 09:49:14 +0700623 :param str body: Body to send with the request
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200624 :param bool chunked: sends the body with chunked encoding
Ghanshyam Mann47a41992019-10-02 16:56:26 +0000625 :param str log_req_body: Whether to log the request body or not.
626 It is default to None which means request
627 body is safe to log otherwise pass any string
628 you want to log in place of request body.
629 For example: '<omitted>'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500630 :rtype: tuple
631 :return: a tuple with the first entry containing the response headers
632 and the second the response body
633 """
634 if headers is None:
635 headers = self.get_headers()
Dan Smith2c192f42023-01-18 11:22:34 -0800636 # In urllib3, chunked only affects the upload. However, we may
637 # want to read large responses to GET incrementally. Re-purpose
638 # chunked=True on a GET to also control how we handle the response.
639 preload = not (method.lower() == 'get' and chunked)
640 if not preload:
641 # NOTE(danms): Not specifically necessary, but don't send
642 # chunked=True to urllib3 on a GET, since it is technically
643 # for PUT/POST type operations
644 chunked = False
Ghanshyam Manncb3cf032019-09-26 00:02:54 +0000645 # Do the actual request, and time it
646 start = time.time()
647 self._log_request_start(method, url)
648 resp, resp_body = self.http_obj.request(
649 url, method, headers=headers,
Dan Smith2c192f42023-01-18 11:22:34 -0800650 body=body, chunked=chunked, preload_content=preload)
Ghanshyam Manncb3cf032019-09-26 00:02:54 +0000651 end = time.time()
Ghanshyam Mann47a41992019-10-02 16:56:26 +0000652 req_body = body if log_req_body is None else log_req_body
Dan Smith2c192f42023-01-18 11:22:34 -0800653 if preload:
654 # NOTE(danms): If we are reading the whole response, we can do
655 # this logging. If not, skip the logging because it will result
656 # in us reading the response data prematurely.
657 self._log_request(method, url, resp, secs=(end - start),
658 req_headers=headers, req_body=req_body,
659 resp_body=resp_body)
Ghanshyam Manncb3cf032019-09-26 00:02:54 +0000660 return resp, resp_body
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500661
662 def request(self, method, url, extra_headers=False, headers=None,
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200663 body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500664 """Send a HTTP request with keystone auth and using the catalog
665
666 This method will send an HTTP request using keystone auth in the
667 headers and the catalog to determine the endpoint to use for the
668 baseurl to send the request to. Additionally
669
670 When a response is received it will check it to see if an error
671 response was received. If it was an exception will be raised to enable
672 it to be handled quickly.
673
674 This method will also handle rate-limiting, if a 413 response code is
675 received it will retry the request after waiting the 'retry-after'
676 duration from the header.
677
678 :param str method: The HTTP verb to use for the request
679 :param str url: Relative url to send the request to
vsaienko9eb846b2016-04-09 00:35:47 +0300680 :param bool extra_headers: Boolean value than indicates if the headers
681 returned by the get_headers() method are to
682 be used but additional headers are needed in
683 the request pass them in as a dict.
zhuflcf35f652018-08-17 10:13:43 +0800684 :param dict headers: Headers to use for the request. If none are
685 specified, then the headers returned from the
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500686 get_headers() method are used. If the request
687 explicitly requires no headers use an empty dict.
Anh Trand44a8be2016-03-25 09:49:14 +0700688 :param str body: Body to send with the request
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200689 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500690 :rtype: tuple
691 :return: a tuple with the first entry containing the response headers
692 and the second the response body
693 :raises UnexpectedContentType: If the content-type of the response
694 isn't an expect type
695 :raises Unauthorized: If a 401 response code is received
696 :raises Forbidden: If a 403 response code is received
697 :raises NotFound: If a 404 response code is received
698 :raises BadRequest: If a 400 response code is received
699 :raises Gone: If a 410 response code is received
700 :raises Conflict: If a 409 response code is received
Kevin Bentona82bc862017-02-13 01:16:13 -0800701 :raises PreconditionFailed: If a 412 response code is received
zhuflcf35f652018-08-17 10:13:43 +0800702 :raises OverLimit: If a 413 response code is received and retry-after
703 is not in the response body or its retry operation
704 exceeds the limits defined by the server
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500705 :raises RateLimitExceeded: If a 413 response code is received and
zhuflcf35f652018-08-17 10:13:43 +0800706 retry-after is in the response body and
707 its retry operation does not exceeds the
708 limits defined by the server
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500709 :raises InvalidContentType: If a 415 response code is received
710 :raises UnprocessableEntity: If a 422 response code is received
711 :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
712 and couldn't be parsed
713 :raises NotImplemented: If a 501 response code is received
714 :raises ServerFault: If a 500 response code is received
715 :raises UnexpectedResponseCode: If a response code above 400 is
716 received and it doesn't fall into any
717 of the handled checks
718 """
719 # if extra_headers is True
720 # default headers would be added to headers
721 retry = 0
722
723 if headers is None:
724 # NOTE(vponomaryov): if some client do not need headers,
725 # it should explicitly pass empty dict
726 headers = self.get_headers()
727 elif extra_headers:
728 try:
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500729 headers.update(self.get_headers())
730 except (ValueError, TypeError):
731 headers = self.get_headers()
732
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200733 resp, resp_body = self._request(method, url, headers=headers,
734 body=body, chunked=chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500735
736 while (resp.status == 413 and
737 'retry-after' in resp and
738 not self.is_absolute_limit(
739 resp, self._parse_resp(resp_body)) and
740 retry < MAX_RECURSION_DEPTH):
741 retry += 1
Paul Glass119565a2016-04-06 11:41:42 -0500742 delay = self._get_retry_after_delay(resp)
743 self.LOG.debug(
744 "Sleeping %s seconds based on retry-after header", delay
745 )
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500746 time.sleep(delay)
747 resp, resp_body = self._request(method, url,
748 headers=headers, body=body)
Ken'ichi Ohmichi17051e82016-10-20 11:46:06 -0700749 self._error_checker(resp, resp_body)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500750 return resp, resp_body
751
Paul Glass119565a2016-04-06 11:41:42 -0500752 def _get_retry_after_delay(self, resp):
753 """Extract the delay from the retry-after header.
754
755 This supports both integer and HTTP date formatted retry-after headers
756 per RFC 2616.
757
758 :param resp: The response containing the retry-after headers
759 :rtype: int
760 :return: The delay in seconds, clamped to be at least 1 second
761 :raises ValueError: On failing to parse the delay
762 """
763 delay = None
764 try:
765 delay = int(resp['retry-after'])
766 except (ValueError, KeyError):
767 pass
768
769 try:
770 retry_timestamp = self._parse_http_date(resp['retry-after'])
771 date_timestamp = self._parse_http_date(resp['date'])
772 delay = int(retry_timestamp - date_timestamp)
773 except (ValueError, OverflowError, KeyError):
774 pass
775
776 if delay is None:
777 raise ValueError(
778 "Failed to parse retry-after header %r as either int or "
779 "HTTP-date." % resp.get('retry-after')
780 )
781
782 # Retry-after headers do not have sub-second precision. Clients may
783 # receive a delay of 0. After sleeping 0 seconds, we would (likely) hit
784 # another 413. To avoid this, always sleep at least 1 second.
785 return max(1, delay)
786
787 def _parse_http_date(self, val):
788 """Parse an HTTP date, like 'Fri, 31 Dec 1999 23:59:59 GMT'.
789
790 Return an epoch timestamp (float), as returned by time.mktime().
791 """
792 parts = email.utils.parsedate(val)
793 if not parts:
794 raise ValueError("Failed to parse date %s" % val)
795 return time.mktime(parts)
796
Ken'ichi Ohmichi17051e82016-10-20 11:46:06 -0700797 def _error_checker(self, resp, resp_body):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500798
799 # NOTE(mtreinish): Check for httplib response from glance_http. The
800 # object can't be used here because importing httplib breaks httplib2.
801 # If another object from a class not imported were passed here as
802 # resp this could possibly fail
803 if str(type(resp)) == "<type 'instance'>":
804 ctype = resp.getheader('content-type')
Dan Smith2c192f42023-01-18 11:22:34 -0800805 elif isinstance(resp, urllib3.HTTPResponse):
806 # If we requested chunked=True streaming, this will be a raw
807 # urllib3.HTTPResponse
808 ctype = resp.getheaders()['content-type']
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500809 else:
810 try:
811 ctype = resp['content-type']
812 # NOTE(mtreinish): Keystone delete user responses doesn't have a
813 # content-type header. (They don't have a body) So just pretend it
814 # is set.
815 except KeyError:
816 ctype = 'application/json'
817
818 # It is not an error response
819 if resp.status < 400:
820 return
821
zhipenghd1db0c72017-02-21 04:40:07 -0500822 # NOTE(zhipengh): There is a purposefully duplicate of content-type
823 # with the only difference is with or without spaces, as specified
824 # in RFC7231.
825 JSON_ENC = ['application/json', 'application/json; charset=utf-8',
826 'application/json;charset=utf-8']
827
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500828 # NOTE(mtreinish): This is for compatibility with Glance and swift
829 # APIs. These are the return content types that Glance api v1
830 # (and occasionally swift) are using.
zhipenghd1db0c72017-02-21 04:40:07 -0500831 # NOTE(zhipengh): There is a purposefully duplicate of content-type
832 # with the only difference is with or without spaces, as specified
833 # in RFC7231.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500834 TXT_ENC = ['text/plain', 'text/html', 'text/html; charset=utf-8',
zhipenghd1db0c72017-02-21 04:40:07 -0500835 'text/plain; charset=utf-8', 'text/html;charset=utf-8',
836 'text/plain;charset=utf-8']
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500837
838 if ctype.lower() in JSON_ENC:
839 parse_resp = True
840 elif ctype.lower() in TXT_ENC:
841 parse_resp = False
842 else:
843 raise exceptions.UnexpectedContentType(str(resp.status),
844 resp=resp)
845
846 if resp.status == 401:
847 if parse_resp:
848 resp_body = self._parse_resp(resp_body)
849 raise exceptions.Unauthorized(resp_body, resp=resp)
850
851 if resp.status == 403:
852 if parse_resp:
853 resp_body = self._parse_resp(resp_body)
854 raise exceptions.Forbidden(resp_body, resp=resp)
855
856 if resp.status == 404:
857 if parse_resp:
858 resp_body = self._parse_resp(resp_body)
859 raise exceptions.NotFound(resp_body, resp=resp)
860
861 if resp.status == 400:
862 if parse_resp:
863 resp_body = self._parse_resp(resp_body)
864 raise exceptions.BadRequest(resp_body, resp=resp)
865
866 if resp.status == 410:
867 if parse_resp:
868 resp_body = self._parse_resp(resp_body)
869 raise exceptions.Gone(resp_body, resp=resp)
870
871 if resp.status == 409:
872 if parse_resp:
873 resp_body = self._parse_resp(resp_body)
874 raise exceptions.Conflict(resp_body, resp=resp)
875
Kevin Bentona82bc862017-02-13 01:16:13 -0800876 if resp.status == 412:
877 if parse_resp:
878 resp_body = self._parse_resp(resp_body)
879 raise exceptions.PreconditionFailed(resp_body, resp=resp)
880
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500881 if resp.status == 413:
882 if parse_resp:
883 resp_body = self._parse_resp(resp_body)
884 if self.is_absolute_limit(resp, resp_body):
885 raise exceptions.OverLimit(resp_body, resp=resp)
886 else:
887 raise exceptions.RateLimitExceeded(resp_body, resp=resp)
888
889 if resp.status == 415:
890 if parse_resp:
891 resp_body = self._parse_resp(resp_body)
892 raise exceptions.InvalidContentType(resp_body, resp=resp)
893
894 if resp.status == 422:
895 if parse_resp:
896 resp_body = self._parse_resp(resp_body)
897 raise exceptions.UnprocessableEntity(resp_body, resp=resp)
898
899 if resp.status in (500, 501):
900 message = resp_body
901 if parse_resp:
902 try:
903 resp_body = self._parse_resp(resp_body)
904 except ValueError:
905 # If response body is a non-json string message.
906 # Use resp_body as is and raise InvalidResponseBody
907 # exception.
908 raise exceptions.InvalidHTTPResponseBody(message)
909 else:
910 if isinstance(resp_body, dict):
911 # I'm seeing both computeFault
912 # and cloudServersFault come back.
913 # Will file a bug to fix, but leave as is for now.
914 if 'cloudServersFault' in resp_body:
915 message = resp_body['cloudServersFault']['message']
916 elif 'computeFault' in resp_body:
917 message = resp_body['computeFault']['message']
918 elif 'error' in resp_body:
919 message = resp_body['error']['message']
920 elif 'message' in resp_body:
921 message = resp_body['message']
922 else:
923 message = resp_body
924
925 if resp.status == 501:
926 raise exceptions.NotImplemented(resp_body, resp=resp,
927 message=message)
928 else:
929 raise exceptions.ServerFault(resp_body, resp=resp,
930 message=message)
931
932 if resp.status >= 400:
933 raise exceptions.UnexpectedResponseCode(str(resp.status),
934 resp=resp)
935
936 def is_absolute_limit(self, resp, resp_body):
Takashi Kajinami6354f612021-07-17 00:37:34 +0900937 if (not isinstance(resp_body, abc.Mapping) or
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500938 'retry-after' not in resp):
939 return True
Paul Glass119565a2016-04-06 11:41:42 -0500940 return 'exceed' in resp_body.get('message', 'blabla')
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500941
Benny Kopilovf5e277c2021-02-08 12:27:37 +0200942 def wait_for_resource_deletion(self, id, *args, **kwargs):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500943 """Waits for a resource to be deleted
944
945 This method will loop over is_resource_deleted until either
946 is_resource_deleted returns True or the build timeout is reached. This
947 depends on is_resource_deleted being implemented
948
949 :param str id: The id of the resource to check
950 :raises TimeoutException: If the build_timeout has elapsed and the
951 resource still hasn't been deleted
952 """
953 start_time = int(time.time())
954 while True:
Benny Kopilovf5e277c2021-02-08 12:27:37 +0200955 if self.is_resource_deleted(id, *args, **kwargs):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500956 return
957 if int(time.time()) - start_time >= self.build_timeout:
958 message = ('Failed to delete %(resource_type)s %(id)s within '
Sampat Ponnagantief552162021-03-17 18:07:36 +0000959 'the required time (%(timeout)s s). Timer started '
Eric Harney941043f2022-01-07 14:21:50 -0500960 'at %(start_time)s. Timer ended at %(end_time)s. '
961 'Waited for %(wait_time)s s.' %
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500962 {'resource_type': self.resource_type, 'id': id,
Sampat Ponnagantief552162021-03-17 18:07:36 +0000963 'timeout': self.build_timeout,
964 'start_time': start_time,
965 'end_time': int(time.time()),
966 'wait_time': int(time.time()) - start_time})
Jordan Pittier9e227c52016-02-09 14:35:18 +0100967 caller = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500968 if caller:
969 message = '(%s) %s' % (caller, message)
970 raise exceptions.TimeoutException(message)
971 time.sleep(self.build_interval)
972
Abhishek Kekane7cff1302020-07-16 10:30:13 +0000973 def wait_for_resource_activation(self, id):
974 """Waits for a resource to become active
975
976 This method will loop over is_resource_active until either
977 is_resource_active returns True or the build timeout is reached. This
978 depends on is_resource_active being implemented
979
980 :param str id: The id of the resource to check
981 :raises TimeoutException: If the build_timeout has elapsed and the
982 resource still hasn't been active
983 """
984 start_time = int(time.time())
985 while True:
986 if self.is_resource_active(id):
987 return
988 if int(time.time()) - start_time >= self.build_timeout:
989 message = ('Failed to reach active state %(resource_type)s '
990 '%(id)s within the required time (%(timeout)s s).' %
991 {'resource_type': self.resource_type, 'id': id,
992 'timeout': self.build_timeout})
993 caller = test_utils.find_test_caller()
994 if caller:
995 message = '(%s) %s' % (caller, message)
996 raise exceptions.TimeoutException(message)
997 time.sleep(self.build_interval)
998
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500999 def is_resource_deleted(self, id):
1000 """Subclasses override with specific deletion detection."""
1001 message = ('"%s" does not implement is_resource_deleted'
1002 % self.__class__.__name__)
1003 raise NotImplementedError(message)
1004
Abhishek Kekane7cff1302020-07-16 10:30:13 +00001005 def is_resource_active(self, id):
1006 """Subclasses override with specific active detection."""
1007 message = ('"%s" does not implement is_resource_active'
1008 % self.__class__.__name__)
1009 raise NotImplementedError(message)
1010
Matthew Treinish9e26ca82016-02-23 11:43:20 -05001011 @property
1012 def resource_type(self):
1013 """Returns the primary type of resource this client works with."""
1014 return 'resource'
1015
1016 @classmethod
1017 def validate_response(cls, schema, resp, body):
1018 # Only check the response if the status code is a success code
1019 # TODO(cyeoh): Eventually we should be able to verify that a failure
1020 # code if it exists is something that we expect. This is explicitly
1021 # declared in the V3 API and so we should be able to export this in
1022 # the response schema. For now we'll ignore it.
1023 if resp.status in HTTP_SUCCESS + HTTP_REDIRECTION:
1024 cls.expected_success(schema['status_code'], resp.status)
1025
1026 # Check the body of a response
1027 body_schema = schema.get('response_body')
1028 if body_schema:
1029 try:
1030 jsonschema.validate(body, body_schema,
1031 cls=JSONSCHEMA_VALIDATOR,
1032 format_checker=FORMAT_CHECKER)
1033 except jsonschema.ValidationError as ex:
guo yunxiana3f55282016-08-10 14:35:16 +08001034 msg = ("HTTP response body is invalid (%s)" % ex)
Matthew Treinish9e26ca82016-02-23 11:43:20 -05001035 raise exceptions.InvalidHTTPResponseBody(msg)
1036 else:
1037 if body:
guo yunxiana3f55282016-08-10 14:35:16 +08001038 msg = ("HTTP response body should not exist (%s)" % body)
Matthew Treinish9e26ca82016-02-23 11:43:20 -05001039 raise exceptions.InvalidHTTPResponseBody(msg)
1040
1041 # Check the header of a response
1042 header_schema = schema.get('response_header')
1043 if header_schema:
1044 try:
1045 jsonschema.validate(resp, header_schema,
1046 cls=JSONSCHEMA_VALIDATOR,
1047 format_checker=FORMAT_CHECKER)
1048 except jsonschema.ValidationError as ex:
guo yunxiana3f55282016-08-10 14:35:16 +08001049 msg = ("HTTP response header is invalid (%s)" % ex)
Matthew Treinish9e26ca82016-02-23 11:43:20 -05001050 raise exceptions.InvalidHTTPResponseHeader(msg)
1051
Ken'ichi Ohmichida26b162017-03-03 15:53:46 -08001052 def _get_base_version_url(self):
1053 # TODO(oomichi): This method can be used for auth's replace_version().
1054 # So it is nice to have common logic for the maintenance.
1055 endpoint = self.base_url
1056 url = urllib.parse.urlsplit(endpoint)
1057 new_path = re.split(r'(^|/)+v\d+(\.\d+)?', url.path)[0]
1058 url = list(url)
1059 url[2] = new_path + '/'
1060 return urllib.parse.urlunsplit(url)
1061
Matthew Treinish9e26ca82016-02-23 11:43:20 -05001062
1063class ResponseBody(dict):
1064 """Class that wraps an http response and dict body into a single value.
1065
1066 Callers that receive this object will normally use it as a dict but
1067 can extract the response if needed.
1068 """
1069
1070 def __init__(self, response, body=None):
1071 body_data = body or {}
1072 self.update(body_data)
1073 self.response = response
1074
1075 def __str__(self):
1076 body = super(ResponseBody, self).__str__()
1077 return "response: %s\nBody: %s" % (self.response, body)
1078
1079
1080class ResponseBodyData(object):
1081 """Class that wraps an http response and string data into a single value.
1082
1083 """
1084
1085 def __init__(self, response, data):
1086 self.response = response
1087 self.data = data
1088
1089 def __str__(self):
1090 return "response: %s\nBody: %s" % (self.response, self.data)
1091
1092
1093class ResponseBodyList(list):
1094 """Class that wraps an http response and list body into a single value.
1095
1096 Callers that receive this object will normally use it as a list but
1097 can extract the response if needed.
1098 """
1099
1100 def __init__(self, response, body=None):
1101 body_data = body or []
1102 self.extend(body_data)
1103 self.response = response
1104
1105 def __str__(self):
1106 body = super(ResponseBodyList, self).__str__()
1107 return "response: %s\nBody: %s" % (self.response, body)