blob: d72b4ddb60559e9cd8bb615fb2129cbc3b00a8c6 [file] [log] [blame]
Matthew Treinish9e26ca82016-02-23 11:43:20 -05001# Copyright 2012 OpenStack Foundation
2# Copyright 2013 IBM Corp.
3# All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
17import collections
Paul Glass119565a2016-04-06 11:41:42 -050018import email.utils
Matthew Treinish9e26ca82016-02-23 11:43:20 -050019import re
20import time
21
22import jsonschema
23from oslo_log import log as logging
24from oslo_serialization import jsonutils as json
25import six
Ken'ichi Ohmichida26b162017-03-03 15:53:46 -080026from six.moves import urllib
Matthew Treinish9e26ca82016-02-23 11:43:20 -050027
28from tempest.lib.common import http
ghanshyamf9ded352016-04-12 17:03:01 +090029from tempest.lib.common import jsonschema_validator
Jordan Pittier9e227c52016-02-09 14:35:18 +010030from tempest.lib.common.utils import test_utils
Matthew Treinish9e26ca82016-02-23 11:43:20 -050031from tempest.lib import exceptions
32
33# redrive rate limited calls at most twice
34MAX_RECURSION_DEPTH = 2
35
36# All the successful HTTP status codes from RFC 7231 & 4918
37HTTP_SUCCESS = (200, 201, 202, 203, 204, 205, 206, 207)
38
39# All the redirection HTTP status codes from RFC 7231 & 4918
40HTTP_REDIRECTION = (300, 301, 302, 303, 304, 305, 306, 307)
41
42# JSON Schema validator and format checker used for JSON Schema validation
ghanshyamf9ded352016-04-12 17:03:01 +090043JSONSCHEMA_VALIDATOR = jsonschema_validator.JSONSCHEMA_VALIDATOR
44FORMAT_CHECKER = jsonschema_validator.FORMAT_CHECKER
Matthew Treinish9e26ca82016-02-23 11:43:20 -050045
46
47class RestClient(object):
48 """Unified OpenStack RestClient class
49
50 This class is used for building openstack api clients on top of. It is
51 intended to provide a base layer for wrapping outgoing http requests in
52 keystone auth as well as providing response code checking and error
53 handling.
54
55 :param auth_provider: an auth provider object used to wrap requests in auth
56 :param str service: The service name to use for the catalog lookup
57 :param str region: The region to use for the catalog lookup
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -060058 :param str name: The endpoint name to use for the catalog lookup; this
59 returns only if the service exists
Matthew Treinish9e26ca82016-02-23 11:43:20 -050060 :param str endpoint_type: The endpoint type to use for the catalog lookup
61 :param int build_interval: Time in seconds between to status checks in
62 wait loops
63 :param int build_timeout: Timeout in seconds to wait for a wait operation.
64 :param bool disable_ssl_certificate_validation: Set to true to disable ssl
65 certificate validation
66 :param str ca_certs: File containing the CA Bundle to use in verifying a
67 TLS server cert
guo yunxian6f24cc42016-07-29 20:03:41 +080068 :param str trace_requests: Regex to use for specifying logging the entirety
Matthew Treinish9e26ca82016-02-23 11:43:20 -050069 of the request and response payload
zhufl071e94c2016-07-12 10:26:34 +080070 :param str http_timeout: Timeout in seconds to wait for the http request to
71 return
Matthew Treinish9e26ca82016-02-23 11:43:20 -050072 """
Matthew Treinish9e26ca82016-02-23 11:43:20 -050073
74 # The version of the API this client implements
75 api_version = None
76
77 LOG = logging.getLogger(__name__)
78
79 def __init__(self, auth_provider, service, region,
80 endpoint_type='publicURL',
81 build_interval=1, build_timeout=60,
82 disable_ssl_certificate_validation=False, ca_certs=None,
zhufl071e94c2016-07-12 10:26:34 +080083 trace_requests='', name=None, http_timeout=None):
Matthew Treinish9e26ca82016-02-23 11:43:20 -050084 self.auth_provider = auth_provider
85 self.service = service
86 self.region = region
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -060087 self.name = name
Matthew Treinish9e26ca82016-02-23 11:43:20 -050088 self.endpoint_type = endpoint_type
89 self.build_interval = build_interval
90 self.build_timeout = build_timeout
91 self.trace_requests = trace_requests
92
93 self._skip_path = False
94 self.general_header_lc = set(('cache-control', 'connection',
95 'date', 'pragma', 'trailer',
96 'transfer-encoding', 'via',
97 'warning'))
98 self.response_header_lc = set(('accept-ranges', 'age', 'etag',
99 'location', 'proxy-authenticate',
100 'retry-after', 'server',
101 'vary', 'www-authenticate'))
102 dscv = disable_ssl_certificate_validation
103 self.http_obj = http.ClosingHttp(
zhufl071e94c2016-07-12 10:26:34 +0800104 disable_ssl_certificate_validation=dscv, ca_certs=ca_certs,
105 timeout=http_timeout)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500106
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500107 def get_headers(self, accept_type=None, send_type=None):
108 """Return the default headers which will be used with outgoing requests
109
110 :param str accept_type: The media type to use for the Accept header, if
111 one isn't provided the object var TYPE will be
112 used
113 :param str send_type: The media-type to use for the Content-Type
114 header, if one isn't provided the object var
115 TYPE will be used
116 :rtype: dict
117 :return: The dictionary of headers which can be used in the headers
118 dict for outgoing request
119 """
120 if accept_type is None:
Masayuki Igawa189b92f2017-04-24 18:57:17 +0900121 accept_type = 'json'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500122 if send_type is None:
Masayuki Igawa189b92f2017-04-24 18:57:17 +0900123 send_type = 'json'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500124 return {'Content-Type': 'application/%s' % send_type,
125 'Accept': 'application/%s' % accept_type}
126
127 def __str__(self):
128 STRING_LIMIT = 80
129 str_format = ("service:%s, base_url:%s, "
130 "filters: %s, build_interval:%s, build_timeout:%s"
131 "\ntoken:%s..., \nheaders:%s...")
132 return str_format % (self.service, self.base_url,
133 self.filters, self.build_interval,
134 self.build_timeout,
135 str(self.token)[0:STRING_LIMIT],
136 str(self.get_headers())[0:STRING_LIMIT])
137
138 @property
139 def user(self):
140 """The username used for requests
141
142 :rtype: string
143 :return: The username being used for requests
144 """
145
146 return self.auth_provider.credentials.username
147
148 @property
149 def user_id(self):
150 """The user_id used for requests
151
152 :rtype: string
153 :return: The user id being used for requests
154 """
155 return self.auth_provider.credentials.user_id
156
157 @property
158 def tenant_name(self):
159 """The tenant/project being used for requests
160
161 :rtype: string
162 :return: The tenant/project name being used for requests
163 """
164 return self.auth_provider.credentials.tenant_name
165
166 @property
167 def tenant_id(self):
168 """The tenant/project id being used for requests
169
170 :rtype: string
171 :return: The tenant/project id being used for requests
172 """
173 return self.auth_provider.credentials.tenant_id
174
175 @property
176 def password(self):
177 """The password being used for requests
178
179 :rtype: string
180 :return: The password being used for requests
181 """
182 return self.auth_provider.credentials.password
183
184 @property
185 def base_url(self):
186 return self.auth_provider.base_url(filters=self.filters)
187
188 @property
189 def token(self):
190 return self.auth_provider.get_token()
191
192 @property
193 def filters(self):
194 _filters = dict(
195 service=self.service,
196 endpoint_type=self.endpoint_type,
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -0600197 region=self.region,
198 name=self.name
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500199 )
200 if self.api_version is not None:
201 _filters['api_version'] = self.api_version
202 if self._skip_path:
203 _filters['skip_path'] = self._skip_path
204 return _filters
205
206 def skip_path(self):
207 """When set, ignore the path part of the base URL from the catalog"""
208 self._skip_path = True
209
210 def reset_path(self):
211 """When reset, use the base URL from the catalog as-is"""
212 self._skip_path = False
213
214 @classmethod
215 def expected_success(cls, expected_code, read_code):
216 """Check expected success response code against the http response
217
218 :param int expected_code: The response code that is expected.
219 Optionally a list of integers can be used
220 to specify multiple valid success codes
221 :param int read_code: The response code which was returned in the
222 response
223 :raises AssertionError: if the expected_code isn't a valid http success
224 response code
225 :raises exceptions.InvalidHttpSuccessCode: if the read code isn't an
226 expected http success code
227 """
ghanshyamc3074202016-04-18 15:20:45 +0900228 if not isinstance(read_code, int):
229 raise TypeError("'read_code' must be an int instead of (%s)"
230 % type(read_code))
231
Hanxi2f977db2016-09-01 17:31:28 +0800232 assert_msg = ("This function only allowed to use for HTTP status "
233 "codes which explicitly defined in the RFC 7231 & 4918. "
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500234 "{0} is not a defined Success Code!"
235 ).format(expected_code)
236 if isinstance(expected_code, list):
237 for code in expected_code:
238 assert code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg
239 else:
240 assert expected_code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg
241
242 # NOTE(afazekas): the http status code above 400 is processed by
243 # the _error_checker method
244 if read_code < 400:
zhufl4d2f5152017-01-17 11:16:12 +0800245 pattern = ("Unexpected http success status code {0}, "
246 "The expected status code is {1}")
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500247 if ((not isinstance(expected_code, list) and
248 (read_code != expected_code)) or
249 (isinstance(expected_code, list) and
250 (read_code not in expected_code))):
251 details = pattern.format(read_code, expected_code)
252 raise exceptions.InvalidHttpSuccessCode(details)
253
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200254 def post(self, url, body, headers=None, extra_headers=False,
255 chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500256 """Send a HTTP POST request using keystone auth
257
258 :param str url: the relative url to send the post request to
259 :param dict body: the request body
260 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300261 :param bool extra_headers: Boolean value than indicates if the headers
262 returned by the get_headers() method are to
263 be used but additional headers are needed in
264 the request pass them in as a dict.
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200265 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500266 :return: a tuple with the first entry containing the response headers
267 and the second the response body
268 :rtype: tuple
269 """
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200270 return self.request('POST', url, extra_headers, headers, body, chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500271
272 def get(self, url, headers=None, extra_headers=False):
273 """Send a HTTP GET request using keystone service catalog and auth
274
275 :param str url: the relative url to send the post request to
276 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300277 :param bool extra_headers: Boolean value than indicates if the headers
278 returned by the get_headers() method are to
279 be used but additional headers are needed in
280 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500281 :return: a tuple with the first entry containing the response headers
282 and the second the response body
283 :rtype: tuple
284 """
285 return self.request('GET', url, extra_headers, headers)
286
287 def delete(self, url, headers=None, body=None, extra_headers=False):
288 """Send a HTTP DELETE request using keystone service catalog and auth
289
290 :param str url: the relative url to send the post request to
291 :param dict headers: The headers to use for the request
292 :param dict body: the request body
vsaienko9eb846b2016-04-09 00:35:47 +0300293 :param bool extra_headers: Boolean value than indicates if the headers
294 returned by the get_headers() method are to
295 be used but additional headers are needed in
296 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500297 :return: a tuple with the first entry containing the response headers
298 and the second the response body
299 :rtype: tuple
300 """
301 return self.request('DELETE', url, extra_headers, headers, body)
302
303 def patch(self, url, body, headers=None, extra_headers=False):
304 """Send a HTTP PATCH request using keystone service catalog and auth
305
306 :param str url: the relative url to send the post request to
307 :param dict body: the request body
308 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300309 :param bool extra_headers: Boolean value than indicates if the headers
310 returned by the get_headers() method are to
311 be used but additional headers are needed in
312 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500313 :return: a tuple with the first entry containing the response headers
314 and the second the response body
315 :rtype: tuple
316 """
317 return self.request('PATCH', url, extra_headers, headers, body)
318
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200319 def put(self, url, body, headers=None, extra_headers=False, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500320 """Send a HTTP PUT request using keystone service catalog and auth
321
322 :param str url: the relative url to send the post request to
323 :param dict body: the request body
324 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300325 :param bool extra_headers: Boolean value than indicates if the headers
326 returned by the get_headers() method are to
327 be used but additional headers are needed in
328 the request pass them in as a dict.
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200329 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500330 :return: a tuple with the first entry containing the response headers
331 and the second the response body
332 :rtype: tuple
333 """
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200334 return self.request('PUT', url, extra_headers, headers, body, chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500335
336 def head(self, url, headers=None, extra_headers=False):
337 """Send a HTTP HEAD request using keystone service catalog and auth
338
339 :param str url: the relative url to send the post request to
340 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300341 :param bool extra_headers: Boolean value than indicates if the headers
342 returned by the get_headers() method are to
343 be used but additional headers are needed in
344 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500345 :return: a tuple with the first entry containing the response headers
346 and the second the response body
347 :rtype: tuple
348 """
349 return self.request('HEAD', url, extra_headers, headers)
350
351 def copy(self, url, headers=None, extra_headers=False):
352 """Send a HTTP COPY request using keystone service catalog and auth
353
354 :param str url: the relative url to send the post request to
355 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300356 :param bool extra_headers: Boolean value than indicates if the headers
357 returned by the get_headers() method are to
358 be used but additional headers are needed in
359 the request pass them in as a dict.
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 """
364 return self.request('COPY', url, extra_headers, headers)
365
366 def get_versions(self):
367 """Get the versions on a endpoint from the keystone catalog
368
369 This method will make a GET request on the baseurl from the keystone
370 catalog to return a list of API versions. It is expected that a GET
371 on the endpoint in the catalog will return a list of supported API
372 versions.
373
374 :return tuple with response headers and list of version numbers
375 :rtype: tuple
376 """
377 resp, body = self.get('')
378 body = self._parse_resp(body)
379 versions = map(lambda x: x['id'], body)
380 return resp, versions
381
382 def _get_request_id(self, resp):
383 for i in ('x-openstack-request-id', 'x-compute-request-id'):
384 if i in resp:
385 return resp[i]
386 return ""
387
388 def _safe_body(self, body, maxlen=4096):
389 # convert a structure into a string safely
390 try:
391 text = six.text_type(body)
392 except UnicodeDecodeError:
393 # if this isn't actually text, return marker that
394 return "<BinaryData: removed>"
395 if len(text) > maxlen:
396 return text[:maxlen]
397 else:
398 return text
399
guo yunxian9f749f92016-08-25 10:55:04 +0800400 def _log_request_start(self, method, req_url):
Jordan Pittier9e227c52016-02-09 14:35:18 +0100401 caller_name = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500402 if self.trace_requests and re.search(self.trace_requests, caller_name):
Jordan Pittier525ec712016-12-07 17:51:26 +0100403 self.LOG.debug('Starting Request (%s): %s %s', caller_name,
404 method, req_url)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500405
guo yunxian9f749f92016-08-25 10:55:04 +0800406 def _log_request_full(self, resp, req_headers=None, req_body=None,
407 resp_body=None, extra=None):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500408 if 'X-Auth-Token' in req_headers:
409 req_headers['X-Auth-Token'] = '<omitted>'
Andrea Frittoli (andreaf)a1edb2d2016-05-10 16:09:59 +0100410 # A shallow copy is sufficient
411 resp_log = resp.copy()
412 if 'x-subject-token' in resp_log:
413 resp_log['x-subject-token'] = '<omitted>'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500414 log_fmt = """Request - Headers: %s
415 Body: %s
416 Response - Headers: %s
417 Body: %s"""
418
419 self.LOG.debug(
Jordan Pittier525ec712016-12-07 17:51:26 +0100420 log_fmt,
421 str(req_headers),
422 self._safe_body(req_body),
423 str(resp_log),
424 self._safe_body(resp_body),
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500425 extra=extra)
426
427 def _log_request(self, method, req_url, resp,
428 secs="", req_headers=None,
429 req_body=None, resp_body=None):
430 if req_headers is None:
431 req_headers = {}
432 # if we have the request id, put it in the right part of the log
433 extra = dict(request_id=self._get_request_id(resp))
434 # NOTE(sdague): while we still have 6 callers to this function
435 # we're going to just provide work around on who is actually
436 # providing timings by gracefully adding no content if they don't.
437 # Once we're down to 1 caller, clean this up.
Jordan Pittier9e227c52016-02-09 14:35:18 +0100438 caller_name = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500439 if secs:
440 secs = " %.3fs" % secs
441 self.LOG.info(
Jordan Pittier525ec712016-12-07 17:51:26 +0100442 'Request (%s): %s %s %s%s',
443 caller_name,
444 resp['status'],
445 method,
446 req_url,
447 secs,
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500448 extra=extra)
449
450 # Also look everything at DEBUG if you want to filter this
451 # out, don't run at debug.
Anusha Raminenif3eb9472017-01-13 08:54:01 +0530452 if self.LOG.isEnabledFor(logging.DEBUG):
guo yunxian9f749f92016-08-25 10:55:04 +0800453 self._log_request_full(resp, req_headers, req_body,
454 resp_body, extra)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500455
456 def _parse_resp(self, body):
457 try:
458 body = json.loads(body)
459 except ValueError:
460 return body
461
462 # We assume, that if the first value of the deserialized body's
463 # item set is a dict or a list, that we just return the first value
464 # of deserialized body.
465 # Essentially "cutting out" the first placeholder element in a body
466 # that looks like this:
467 #
468 # {
469 # "users": [
470 # ...
471 # ]
472 # }
473 try:
474 # Ensure there are not more than one top-level keys
475 # NOTE(freerunner): Ensure, that JSON is not nullable to
476 # to prevent StopIteration Exception
477 if len(body.keys()) != 1:
478 return body
479 # Just return the "wrapped" element
480 first_key, first_item = six.next(six.iteritems(body))
481 if isinstance(first_item, (dict, list)):
482 return first_item
483 except (ValueError, IndexError):
484 pass
485 return body
486
487 def response_checker(self, method, resp, resp_body):
488 """A sanity check on the response from a HTTP request
489
490 This method does a sanity check on whether the response from an HTTP
491 request conforms the HTTP RFC.
492
493 :param str method: The HTTP verb of the request associated with the
494 response being passed in.
495 :param resp: The response headers
496 :param resp_body: The body of the response
497 :raises ResponseWithNonEmptyBody: If the response with the status code
498 is not supposed to have a body
499 :raises ResponseWithEntity: If the response code is 205 but has an
500 entity
501 """
502 if (resp.status in set((204, 205, 304)) or resp.status < 200 or
503 method.upper() == 'HEAD') and resp_body:
504 raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
505 # NOTE(afazekas):
506 # If the HTTP Status Code is 205
507 # 'The response MUST NOT include an entity.'
508 # A HTTP entity has an entity-body and an 'entity-header'.
509 # In the HTTP response specification (Section 6) the 'entity-header'
510 # 'generic-header' and 'response-header' are in OR relation.
511 # All headers not in the above two group are considered as entity
512 # header in every interpretation.
513
514 if (resp.status == 205 and
515 0 != len(set(resp.keys()) - set(('status',)) -
516 self.response_header_lc - self.general_header_lc)):
517 raise exceptions.ResponseWithEntity()
518 # NOTE(afazekas)
519 # Now the swift sometimes (delete not empty container)
520 # returns with non json error response, we can create new rest class
521 # for swift.
522 # Usually RFC2616 says error responses SHOULD contain an explanation.
523 # The warning is normal for SHOULD/SHOULD NOT case
524
525 # Likely it will cause an error
526 if method != 'HEAD' and not resp_body and resp.status >= 400:
527 self.LOG.warning("status >= 400 response with empty body")
528
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200529 def _request(self, method, url, headers=None, body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500530 """A simple HTTP request interface."""
531 # Authenticate the request with the auth provider
532 req_url, req_headers, req_body = self.auth_provider.auth_request(
533 method, url, headers, body, self.filters)
534
535 # Do the actual request, and time it
536 start = time.time()
537 self._log_request_start(method, req_url)
538 resp, resp_body = self.raw_request(
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200539 req_url, method, headers=req_headers, body=req_body,
540 chunked=chunked
541 )
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500542 end = time.time()
543 self._log_request(method, req_url, resp, secs=(end - start),
544 req_headers=req_headers, req_body=req_body,
545 resp_body=resp_body)
546
547 # Verify HTTP response codes
548 self.response_checker(method, resp, resp_body)
549
550 return resp, resp_body
551
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200552 def raw_request(self, url, method, headers=None, body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500553 """Send a raw HTTP request without the keystone catalog or auth
554
555 This method sends a HTTP request in the same manner as the request()
556 method, however it does so without using keystone auth or the catalog
557 to determine the base url. Additionally no response handling is done
558 the results from the request are just returned.
559
560 :param str url: Full url to send the request
561 :param str method: The HTTP verb to use for the request
562 :param str headers: Headers to use for the request if none are specifed
563 the headers
Anh Trand44a8be2016-03-25 09:49:14 +0700564 :param str body: Body to send with the request
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200565 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500566 :rtype: tuple
567 :return: a tuple with the first entry containing the response headers
568 and the second the response body
569 """
570 if headers is None:
571 headers = self.get_headers()
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200572 return self.http_obj.request(url, method, headers=headers,
573 body=body, chunked=chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500574
575 def request(self, method, url, extra_headers=False, headers=None,
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200576 body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500577 """Send a HTTP request with keystone auth and using the catalog
578
579 This method will send an HTTP request using keystone auth in the
580 headers and the catalog to determine the endpoint to use for the
581 baseurl to send the request to. Additionally
582
583 When a response is received it will check it to see if an error
584 response was received. If it was an exception will be raised to enable
585 it to be handled quickly.
586
587 This method will also handle rate-limiting, if a 413 response code is
588 received it will retry the request after waiting the 'retry-after'
589 duration from the header.
590
591 :param str method: The HTTP verb to use for the request
592 :param str url: Relative url to send the request to
vsaienko9eb846b2016-04-09 00:35:47 +0300593 :param bool extra_headers: Boolean value than indicates if the headers
594 returned by the get_headers() method are to
595 be used but additional headers are needed in
596 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500597 :param dict headers: Headers to use for the request if none are
598 specifed the headers returned from the
599 get_headers() method are used. If the request
600 explicitly requires no headers use an empty dict.
Anh Trand44a8be2016-03-25 09:49:14 +0700601 :param str body: Body to send with the request
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200602 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500603 :rtype: tuple
604 :return: a tuple with the first entry containing the response headers
605 and the second the response body
606 :raises UnexpectedContentType: If the content-type of the response
607 isn't an expect type
608 :raises Unauthorized: If a 401 response code is received
609 :raises Forbidden: If a 403 response code is received
610 :raises NotFound: If a 404 response code is received
611 :raises BadRequest: If a 400 response code is received
612 :raises Gone: If a 410 response code is received
613 :raises Conflict: If a 409 response code is received
Kevin Bentona82bc862017-02-13 01:16:13 -0800614 :raises PreconditionFailed: If a 412 response code is received
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500615 :raises OverLimit: If a 413 response code is received and over_limit is
616 not in the response body
617 :raises RateLimitExceeded: If a 413 response code is received and
618 over_limit is in the response body
619 :raises InvalidContentType: If a 415 response code is received
620 :raises UnprocessableEntity: If a 422 response code is received
621 :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
622 and couldn't be parsed
623 :raises NotImplemented: If a 501 response code is received
624 :raises ServerFault: If a 500 response code is received
625 :raises UnexpectedResponseCode: If a response code above 400 is
626 received and it doesn't fall into any
627 of the handled checks
628 """
629 # if extra_headers is True
630 # default headers would be added to headers
631 retry = 0
632
633 if headers is None:
634 # NOTE(vponomaryov): if some client do not need headers,
635 # it should explicitly pass empty dict
636 headers = self.get_headers()
637 elif extra_headers:
638 try:
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500639 headers.update(self.get_headers())
640 except (ValueError, TypeError):
641 headers = self.get_headers()
642
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200643 resp, resp_body = self._request(method, url, headers=headers,
644 body=body, chunked=chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500645
646 while (resp.status == 413 and
647 'retry-after' in resp and
648 not self.is_absolute_limit(
649 resp, self._parse_resp(resp_body)) and
650 retry < MAX_RECURSION_DEPTH):
651 retry += 1
Paul Glass119565a2016-04-06 11:41:42 -0500652 delay = self._get_retry_after_delay(resp)
653 self.LOG.debug(
654 "Sleeping %s seconds based on retry-after header", delay
655 )
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500656 time.sleep(delay)
657 resp, resp_body = self._request(method, url,
658 headers=headers, body=body)
Ken'ichi Ohmichi17051e82016-10-20 11:46:06 -0700659 self._error_checker(resp, resp_body)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500660 return resp, resp_body
661
Paul Glass119565a2016-04-06 11:41:42 -0500662 def _get_retry_after_delay(self, resp):
663 """Extract the delay from the retry-after header.
664
665 This supports both integer and HTTP date formatted retry-after headers
666 per RFC 2616.
667
668 :param resp: The response containing the retry-after headers
669 :rtype: int
670 :return: The delay in seconds, clamped to be at least 1 second
671 :raises ValueError: On failing to parse the delay
672 """
673 delay = None
674 try:
675 delay = int(resp['retry-after'])
676 except (ValueError, KeyError):
677 pass
678
679 try:
680 retry_timestamp = self._parse_http_date(resp['retry-after'])
681 date_timestamp = self._parse_http_date(resp['date'])
682 delay = int(retry_timestamp - date_timestamp)
683 except (ValueError, OverflowError, KeyError):
684 pass
685
686 if delay is None:
687 raise ValueError(
688 "Failed to parse retry-after header %r as either int or "
689 "HTTP-date." % resp.get('retry-after')
690 )
691
692 # Retry-after headers do not have sub-second precision. Clients may
693 # receive a delay of 0. After sleeping 0 seconds, we would (likely) hit
694 # another 413. To avoid this, always sleep at least 1 second.
695 return max(1, delay)
696
697 def _parse_http_date(self, val):
698 """Parse an HTTP date, like 'Fri, 31 Dec 1999 23:59:59 GMT'.
699
700 Return an epoch timestamp (float), as returned by time.mktime().
701 """
702 parts = email.utils.parsedate(val)
703 if not parts:
704 raise ValueError("Failed to parse date %s" % val)
705 return time.mktime(parts)
706
Ken'ichi Ohmichi17051e82016-10-20 11:46:06 -0700707 def _error_checker(self, resp, resp_body):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500708
709 # NOTE(mtreinish): Check for httplib response from glance_http. The
710 # object can't be used here because importing httplib breaks httplib2.
711 # If another object from a class not imported were passed here as
712 # resp this could possibly fail
713 if str(type(resp)) == "<type 'instance'>":
714 ctype = resp.getheader('content-type')
715 else:
716 try:
717 ctype = resp['content-type']
718 # NOTE(mtreinish): Keystone delete user responses doesn't have a
719 # content-type header. (They don't have a body) So just pretend it
720 # is set.
721 except KeyError:
722 ctype = 'application/json'
723
724 # It is not an error response
725 if resp.status < 400:
726 return
727
zhipenghd1db0c72017-02-21 04:40:07 -0500728 # NOTE(zhipengh): There is a purposefully duplicate of content-type
729 # with the only difference is with or without spaces, as specified
730 # in RFC7231.
731 JSON_ENC = ['application/json', 'application/json; charset=utf-8',
732 'application/json;charset=utf-8']
733
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500734 # NOTE(mtreinish): This is for compatibility with Glance and swift
735 # APIs. These are the return content types that Glance api v1
736 # (and occasionally swift) are using.
zhipenghd1db0c72017-02-21 04:40:07 -0500737 # NOTE(zhipengh): There is a purposefully duplicate of content-type
738 # with the only difference is with or without spaces, as specified
739 # in RFC7231.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500740 TXT_ENC = ['text/plain', 'text/html', 'text/html; charset=utf-8',
zhipenghd1db0c72017-02-21 04:40:07 -0500741 'text/plain; charset=utf-8', 'text/html;charset=utf-8',
742 'text/plain;charset=utf-8']
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500743
744 if ctype.lower() in JSON_ENC:
745 parse_resp = True
746 elif ctype.lower() in TXT_ENC:
747 parse_resp = False
748 else:
749 raise exceptions.UnexpectedContentType(str(resp.status),
750 resp=resp)
751
752 if resp.status == 401:
753 if parse_resp:
754 resp_body = self._parse_resp(resp_body)
755 raise exceptions.Unauthorized(resp_body, resp=resp)
756
757 if resp.status == 403:
758 if parse_resp:
759 resp_body = self._parse_resp(resp_body)
760 raise exceptions.Forbidden(resp_body, resp=resp)
761
762 if resp.status == 404:
763 if parse_resp:
764 resp_body = self._parse_resp(resp_body)
765 raise exceptions.NotFound(resp_body, resp=resp)
766
767 if resp.status == 400:
768 if parse_resp:
769 resp_body = self._parse_resp(resp_body)
770 raise exceptions.BadRequest(resp_body, resp=resp)
771
772 if resp.status == 410:
773 if parse_resp:
774 resp_body = self._parse_resp(resp_body)
775 raise exceptions.Gone(resp_body, resp=resp)
776
777 if resp.status == 409:
778 if parse_resp:
779 resp_body = self._parse_resp(resp_body)
780 raise exceptions.Conflict(resp_body, resp=resp)
781
Kevin Bentona82bc862017-02-13 01:16:13 -0800782 if resp.status == 412:
783 if parse_resp:
784 resp_body = self._parse_resp(resp_body)
785 raise exceptions.PreconditionFailed(resp_body, resp=resp)
786
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500787 if resp.status == 413:
788 if parse_resp:
789 resp_body = self._parse_resp(resp_body)
790 if self.is_absolute_limit(resp, resp_body):
791 raise exceptions.OverLimit(resp_body, resp=resp)
792 else:
793 raise exceptions.RateLimitExceeded(resp_body, resp=resp)
794
795 if resp.status == 415:
796 if parse_resp:
797 resp_body = self._parse_resp(resp_body)
798 raise exceptions.InvalidContentType(resp_body, resp=resp)
799
800 if resp.status == 422:
801 if parse_resp:
802 resp_body = self._parse_resp(resp_body)
803 raise exceptions.UnprocessableEntity(resp_body, resp=resp)
804
805 if resp.status in (500, 501):
806 message = resp_body
807 if parse_resp:
808 try:
809 resp_body = self._parse_resp(resp_body)
810 except ValueError:
811 # If response body is a non-json string message.
812 # Use resp_body as is and raise InvalidResponseBody
813 # exception.
814 raise exceptions.InvalidHTTPResponseBody(message)
815 else:
816 if isinstance(resp_body, dict):
817 # I'm seeing both computeFault
818 # and cloudServersFault come back.
819 # Will file a bug to fix, but leave as is for now.
820 if 'cloudServersFault' in resp_body:
821 message = resp_body['cloudServersFault']['message']
822 elif 'computeFault' in resp_body:
823 message = resp_body['computeFault']['message']
824 elif 'error' in resp_body:
825 message = resp_body['error']['message']
826 elif 'message' in resp_body:
827 message = resp_body['message']
828 else:
829 message = resp_body
830
831 if resp.status == 501:
832 raise exceptions.NotImplemented(resp_body, resp=resp,
833 message=message)
834 else:
835 raise exceptions.ServerFault(resp_body, resp=resp,
836 message=message)
837
838 if resp.status >= 400:
839 raise exceptions.UnexpectedResponseCode(str(resp.status),
840 resp=resp)
841
842 def is_absolute_limit(self, resp, resp_body):
843 if (not isinstance(resp_body, collections.Mapping) or
844 'retry-after' not in resp):
845 return True
Paul Glass119565a2016-04-06 11:41:42 -0500846 return 'exceed' in resp_body.get('message', 'blabla')
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500847
848 def wait_for_resource_deletion(self, id):
849 """Waits for a resource to be deleted
850
851 This method will loop over is_resource_deleted until either
852 is_resource_deleted returns True or the build timeout is reached. This
853 depends on is_resource_deleted being implemented
854
855 :param str id: The id of the resource to check
856 :raises TimeoutException: If the build_timeout has elapsed and the
857 resource still hasn't been deleted
858 """
859 start_time = int(time.time())
860 while True:
861 if self.is_resource_deleted(id):
862 return
863 if int(time.time()) - start_time >= self.build_timeout:
864 message = ('Failed to delete %(resource_type)s %(id)s within '
865 'the required time (%(timeout)s s).' %
866 {'resource_type': self.resource_type, 'id': id,
867 'timeout': self.build_timeout})
Jordan Pittier9e227c52016-02-09 14:35:18 +0100868 caller = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500869 if caller:
870 message = '(%s) %s' % (caller, message)
871 raise exceptions.TimeoutException(message)
872 time.sleep(self.build_interval)
873
874 def is_resource_deleted(self, id):
875 """Subclasses override with specific deletion detection."""
876 message = ('"%s" does not implement is_resource_deleted'
877 % self.__class__.__name__)
878 raise NotImplementedError(message)
879
880 @property
881 def resource_type(self):
882 """Returns the primary type of resource this client works with."""
883 return 'resource'
884
885 @classmethod
886 def validate_response(cls, schema, resp, body):
887 # Only check the response if the status code is a success code
888 # TODO(cyeoh): Eventually we should be able to verify that a failure
889 # code if it exists is something that we expect. This is explicitly
890 # declared in the V3 API and so we should be able to export this in
891 # the response schema. For now we'll ignore it.
892 if resp.status in HTTP_SUCCESS + HTTP_REDIRECTION:
893 cls.expected_success(schema['status_code'], resp.status)
894
895 # Check the body of a response
896 body_schema = schema.get('response_body')
897 if body_schema:
898 try:
899 jsonschema.validate(body, body_schema,
900 cls=JSONSCHEMA_VALIDATOR,
901 format_checker=FORMAT_CHECKER)
902 except jsonschema.ValidationError as ex:
guo yunxiana3f55282016-08-10 14:35:16 +0800903 msg = ("HTTP response body is invalid (%s)" % ex)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500904 raise exceptions.InvalidHTTPResponseBody(msg)
905 else:
906 if body:
guo yunxiana3f55282016-08-10 14:35:16 +0800907 msg = ("HTTP response body should not exist (%s)" % body)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500908 raise exceptions.InvalidHTTPResponseBody(msg)
909
910 # Check the header of a response
911 header_schema = schema.get('response_header')
912 if header_schema:
913 try:
914 jsonschema.validate(resp, header_schema,
915 cls=JSONSCHEMA_VALIDATOR,
916 format_checker=FORMAT_CHECKER)
917 except jsonschema.ValidationError as ex:
guo yunxiana3f55282016-08-10 14:35:16 +0800918 msg = ("HTTP response header is invalid (%s)" % ex)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500919 raise exceptions.InvalidHTTPResponseHeader(msg)
920
Ken'ichi Ohmichida26b162017-03-03 15:53:46 -0800921 def _get_base_version_url(self):
922 # TODO(oomichi): This method can be used for auth's replace_version().
923 # So it is nice to have common logic for the maintenance.
924 endpoint = self.base_url
925 url = urllib.parse.urlsplit(endpoint)
926 new_path = re.split(r'(^|/)+v\d+(\.\d+)?', url.path)[0]
927 url = list(url)
928 url[2] = new_path + '/'
929 return urllib.parse.urlunsplit(url)
930
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500931
932class ResponseBody(dict):
933 """Class that wraps an http response and dict body into a single value.
934
935 Callers that receive this object will normally use it as a dict but
936 can extract the response if needed.
937 """
938
939 def __init__(self, response, body=None):
940 body_data = body or {}
941 self.update(body_data)
942 self.response = response
943
944 def __str__(self):
945 body = super(ResponseBody, self).__str__()
946 return "response: %s\nBody: %s" % (self.response, body)
947
948
949class ResponseBodyData(object):
950 """Class that wraps an http response and string data into a single value.
951
952 """
953
954 def __init__(self, response, data):
955 self.response = response
956 self.data = data
957
958 def __str__(self):
959 return "response: %s\nBody: %s" % (self.response, self.data)
960
961
962class ResponseBodyList(list):
963 """Class that wraps an http response and list body into a single value.
964
965 Callers that receive this object will normally use it as a list but
966 can extract the response if needed.
967 """
968
969 def __init__(self, response, body=None):
970 body_data = body or []
971 self.extend(body_data)
972 self.response = response
973
974 def __str__(self):
975 body = super(ResponseBodyList, self).__str__()
976 return "response: %s\nBody: %s" % (self.response, body)