blob: 3802c9d5385a372f1a7603c918aaa2f09893518b [file] [log] [blame]
ZhiQiang Fan39f97222013-09-20 04:49:44 +08001# Copyright 2012 OpenStack Foundation
Brant Knudsonc7ca3342013-03-28 21:08:50 -05002# Copyright 2013 IBM Corp.
Jay Pipes3f981df2012-03-27 18:59:44 -04003# 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
Attila Fazekas55f6d8c2013-03-10 10:32:54 +010017import collections
Matthew Treinisha83a16e2012-12-07 13:44:02 -050018import json
Joe Gordon75d2e622014-10-14 12:31:22 -070019import logging as real_logging
Attila Fazekas11d2a772013-01-29 17:46:52 +010020import re
Eoghan Glynna5598972012-03-01 09:27:17 -050021import time
Jay Pipes3f981df2012-03-27 18:59:44 -040022
Chris Yeohc266b282014-03-13 18:19:00 +103023import jsonschema
Sean Dague4f8d7022014-09-25 10:27:13 -040024import six
Chris Yeohc266b282014-03-13 18:19:00 +103025
Mate Lakat23a58a32013-08-23 02:06:22 +010026from tempest.common import http
Matt Riedemann7efa5c32014-05-02 13:35:44 -070027from tempest.common.utils import misc as misc_utils
Matthew Treinish684d8992014-01-30 16:27:40 +000028from tempest import config
Daryl Wallecked8bef32011-12-05 23:02:08 -060029from tempest import exceptions
Matthew Treinishf4a9b0f2013-07-26 16:58:26 -040030from tempest.openstack.common import log as logging
Daryl Walleck1465d612011-11-02 02:22:15 -050031
Matthew Treinish684d8992014-01-30 16:27:40 +000032CONF = config.CONF
33
Eoghan Glynna5598972012-03-01 09:27:17 -050034# redrive rate limited calls at most twice
35MAX_RECURSION_DEPTH = 2
36
Ghanshyam25ad2c32014-07-17 16:44:55 +090037# All the successful HTTP status codes from RFC 7231 & 4918
38HTTP_SUCCESS = (200, 201, 202, 203, 204, 205, 206, 207)
Attila Fazekas54a42862013-07-28 22:31:06 +020039
Eoghan Glynna5598972012-03-01 09:27:17 -050040
David Kranz70f137c2014-10-23 17:57:18 -040041class ResponseBody(dict):
David Kranz291bf792014-12-02 10:31:40 -050042 """Class that wraps an http response and dict body into a single value.
David Kranz70f137c2014-10-23 17:57:18 -040043
44 Callers that receive this object will normally use it as a dict but
45 can extract the response if needed.
46 """
47
48 def __init__(self, response, body=None):
49 body_data = body or {}
50 self.update(body_data)
51 self.response = response
52
53 def __str__(self):
54 body = super.__str__(self)
David Kranzf43babe2014-12-01 11:33:07 -050055 return "response: %s\nBody: %s" % (self.response, body)
David Kranz70f137c2014-10-23 17:57:18 -040056
57
David Kranz291bf792014-12-02 10:31:40 -050058class ResponseBodyList(list):
59 """Class that wraps an http response and list body into a single value.
60
61 Callers that receive this object will normally use it as a list but
62 can extract the response if needed.
63 """
64
65 def __init__(self, response, body=None):
66 body_data = body or []
67 self.extend(body_data)
68 self.response = response
69
70 def __str__(self):
71 body = super.__str__(self)
72 return "response: %s\nBody: %s" % (self.response, body)
73
74
Daryl Walleck1465d612011-11-02 02:22:15 -050075class RestClient(object):
vponomaryov67b58fe2014-02-06 19:05:41 +020076
Dan Smithba6cb162012-08-14 07:22:42 -070077 TYPE = "json"
vponomaryov67b58fe2014-02-06 19:05:41 +020078
Attila Fazekas11d2a772013-01-29 17:46:52 +010079 LOG = logging.getLogger(__name__)
Daryl Walleck1465d612011-11-02 02:22:15 -050080
Ken'ichi Ohmichie9f50412015-01-05 04:57:26 +000081 def __init__(self, auth_provider, service, region,
82 endpoint_type='publicURL',
Ken'ichi Ohmichi0cd316b2014-12-24 03:51:04 +000083 build_interval=1, build_timeout=60):
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000084 self.auth_provider = auth_provider
Ken'ichi Ohmichi0cd316b2014-12-24 03:51:04 +000085 self.service = service
Ken'ichi Ohmichie9f50412015-01-05 04:57:26 +000086 self.region = region
Ken'ichi Ohmichi0690ea42015-01-02 07:03:51 +000087 self.endpoint_type = endpoint_type
Ken'ichi Ohmichi0cd316b2014-12-24 03:51:04 +000088 self.build_interval = build_interval
89 self.build_timeout = build_timeout
chris fattarsi5098fa22012-04-17 13:27:00 -070090
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000091 # The version of the API this client implements
92 self.api_version = None
93 self._skip_path = False
Attila Fazekas72c7a5f2012-12-03 17:17:23 +010094 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'))
Matthew Treinish684d8992014-01-30 16:27:40 +0000102 dscv = CONF.identity.disable_ssl_certificate_validation
Rob Crittendena7db6692014-11-23 18:44:38 -0500103 ca_certs = CONF.identity.ca_certificates_file
Mate Lakat23a58a32013-08-23 02:06:22 +0100104 self.http_obj = http.ClosingHttp(
Rob Crittendena7db6692014-11-23 18:44:38 -0500105 disable_ssl_certificate_validation=dscv, ca_certs=ca_certs)
chris fattarsi5098fa22012-04-17 13:27:00 -0700106
vponomaryov67b58fe2014-02-06 19:05:41 +0200107 def _get_type(self):
108 return self.TYPE
109
110 def get_headers(self, accept_type=None, send_type=None):
vponomaryov67b58fe2014-02-06 19:05:41 +0200111 if accept_type is None:
112 accept_type = self._get_type()
113 if send_type is None:
114 send_type = self._get_type()
115 return {'Content-Type': 'application/%s' % send_type,
116 'Accept': 'application/%s' % accept_type}
117
DennyZhang7be75002013-09-19 06:55:11 -0500118 def __str__(self):
119 STRING_LIMIT = 80
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000120 str_format = ("config:%s, service:%s, base_url:%s, "
121 "filters: %s, build_interval:%s, build_timeout:%s"
DennyZhang7be75002013-09-19 06:55:11 -0500122 "\ntoken:%s..., \nheaders:%s...")
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000123 return str_format % (CONF, self.service, self.base_url,
124 self.filters, self.build_interval,
125 self.build_timeout,
DennyZhang7be75002013-09-19 06:55:11 -0500126 str(self.token)[0:STRING_LIMIT],
vponomaryov67b58fe2014-02-06 19:05:41 +0200127 str(self.get_headers())[0:STRING_LIMIT])
DennyZhang7be75002013-09-19 06:55:11 -0500128
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000129 @property
130 def user(self):
Andrea Frittoli86ad28d2014-03-20 10:09:12 +0000131 return self.auth_provider.credentials.username
Li Ma216550f2013-06-12 11:26:08 -0700132
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000133 @property
Andrea Frittoli9612e812014-03-13 10:57:26 +0000134 def user_id(self):
135 return self.auth_provider.credentials.user_id
136
137 @property
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000138 def tenant_name(self):
Andrea Frittoli86ad28d2014-03-20 10:09:12 +0000139 return self.auth_provider.credentials.tenant_name
140
141 @property
142 def tenant_id(self):
143 return self.auth_provider.credentials.tenant_id
chris fattarsi5098fa22012-04-17 13:27:00 -0700144
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000145 @property
146 def password(self):
Andrea Frittoli86ad28d2014-03-20 10:09:12 +0000147 return self.auth_provider.credentials.password
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000148
149 @property
150 def base_url(self):
151 return self.auth_provider.base_url(filters=self.filters)
152
153 @property
Andrea Frittoli77f9da42014-02-06 11:18:19 +0000154 def token(self):
155 return self.auth_provider.get_token()
156
157 @property
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000158 def filters(self):
159 _filters = dict(
160 service=self.service,
Ken'ichi Ohmichi0690ea42015-01-02 07:03:51 +0000161 endpoint_type=self.endpoint_type,
Ken'ichi Ohmichie9f50412015-01-05 04:57:26 +0000162 region=self.region
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000163 )
164 if self.api_version is not None:
165 _filters['api_version'] = self.api_version
166 if self._skip_path:
167 _filters['skip_path'] = self._skip_path
168 return _filters
169
170 def skip_path(self):
chris fattarsi5098fa22012-04-17 13:27:00 -0700171 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000172 When set, ignore the path part of the base URL from the catalog
chris fattarsi5098fa22012-04-17 13:27:00 -0700173 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000174 self._skip_path = True
chris fattarsi5098fa22012-04-17 13:27:00 -0700175
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000176 def reset_path(self):
Attila Fazekasb2902af2013-02-16 16:22:44 +0100177 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000178 When reset, use the base URL from the catalog as-is
Daryl Walleck1465d612011-11-02 02:22:15 -0500179 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000180 self._skip_path = False
Brant Knudsonc7ca3342013-03-28 21:08:50 -0500181
Matthew Treinish2b2483e2014-05-08 23:26:10 -0400182 @classmethod
183 def expected_success(cls, expected_code, read_code):
Attila Fazekas54a42862013-07-28 22:31:06 +0200184 assert_msg = ("This function only allowed to use for HTTP status"
Ghanshyam25ad2c32014-07-17 16:44:55 +0900185 "codes which explicitly defined in the RFC 7231 & 4918."
186 "{0} is not a defined Success Code!"
187 ).format(expected_code)
Matthew Treinish2b2483e2014-05-08 23:26:10 -0400188 if isinstance(expected_code, list):
189 for code in expected_code:
190 assert code in HTTP_SUCCESS, assert_msg
191 else:
192 assert expected_code in HTTP_SUCCESS, assert_msg
Attila Fazekas54a42862013-07-28 22:31:06 +0200193
194 # NOTE(afazekas): the http status code above 400 is processed by
195 # the _error_checker method
Matthew Treinish2b2483e2014-05-08 23:26:10 -0400196 if read_code < 400:
197 pattern = """Unexpected http success status code {0},
198 The expected status code is {1}"""
199 if ((not isinstance(expected_code, list) and
Matthew Treinish1d14c542014-06-17 20:25:40 -0400200 (read_code != expected_code)) or
201 (isinstance(expected_code, list) and
202 (read_code not in expected_code))):
Attila Fazekas54a42862013-07-28 22:31:06 +0200203 details = pattern.format(read_code, expected_code)
204 raise exceptions.InvalidHttpSuccessCode(details)
205
Sergey Murashov4fccd322014-03-22 09:58:52 +0400206 def post(self, url, body, headers=None, extra_headers=False):
207 return self.request('POST', url, extra_headers, headers, body)
Daryl Walleck1465d612011-11-02 02:22:15 -0500208
Sergey Murashov4fccd322014-03-22 09:58:52 +0400209 def get(self, url, headers=None, extra_headers=False):
210 return self.request('GET', url, extra_headers, headers)
Daryl Walleck1465d612011-11-02 02:22:15 -0500211
Sergey Murashov4fccd322014-03-22 09:58:52 +0400212 def delete(self, url, headers=None, body=None, extra_headers=False):
213 return self.request('DELETE', url, extra_headers, headers, body)
Daryl Walleck1465d612011-11-02 02:22:15 -0500214
Sergey Murashov4fccd322014-03-22 09:58:52 +0400215 def patch(self, url, body, headers=None, extra_headers=False):
216 return self.request('PATCH', url, extra_headers, headers, body)
rajalakshmi-ganesanab426722013-02-08 15:49:15 +0530217
Sergey Murashov4fccd322014-03-22 09:58:52 +0400218 def put(self, url, body, headers=None, extra_headers=False):
219 return self.request('PUT', url, extra_headers, headers, body)
Daryl Walleck1465d612011-11-02 02:22:15 -0500220
Sergey Murashov4fccd322014-03-22 09:58:52 +0400221 def head(self, url, headers=None, extra_headers=False):
222 return self.request('HEAD', url, extra_headers, headers)
Larisa Ustalov6c3c7802012-11-05 12:25:19 +0200223
Sergey Murashov4fccd322014-03-22 09:58:52 +0400224 def copy(self, url, headers=None, extra_headers=False):
225 return self.request('COPY', url, extra_headers, headers)
dwalleck5d734432012-10-04 01:11:47 -0500226
Matthew Treinishc0f768f2013-03-11 14:24:16 -0400227 def get_versions(self):
228 resp, body = self.get('')
229 body = self._parse_resp(body)
Matthew Treinishc0f768f2013-03-11 14:24:16 -0400230 versions = map(lambda x: x['id'], body)
231 return resp, versions
232
Sean Dague89a85912014-03-19 16:37:29 -0400233 def _get_request_id(self, resp):
234 for i in ('x-openstack-request-id', 'x-compute-request-id'):
235 if i in resp:
236 return resp[i]
237 return ""
Attila Fazekas11d2a772013-01-29 17:46:52 +0100238
Ken'ichi Ohmichie9140bf2014-12-10 05:31:16 +0000239 def _safe_body(self, body, maxlen=4096):
240 # convert a structure into a string safely
241 try:
242 text = six.text_type(body)
243 except UnicodeDecodeError:
244 # if this isn't actually text, return marker that
245 return "<BinaryData: removed>"
246 if len(text) > maxlen:
247 return text[:maxlen]
248 else:
249 return text
250
Ghanshyam2a180b82014-06-16 13:54:22 +0900251 def _log_request_start(self, method, req_url, req_headers=None,
Sean Dague2cb56992014-05-29 08:17:42 -0400252 req_body=None):
Ghanshyam2a180b82014-06-16 13:54:22 +0900253 if req_headers is None:
254 req_headers = {}
Sean Dague2cb56992014-05-29 08:17:42 -0400255 caller_name = misc_utils.find_test_caller()
256 trace_regex = CONF.debug.trace_requests
257 if trace_regex and re.search(trace_regex, caller_name):
258 self.LOG.debug('Starting Request (%s): %s %s' %
259 (caller_name, method, req_url))
260
Sean Dague4f8d7022014-09-25 10:27:13 -0400261 def _log_request_full(self, method, req_url, resp,
262 secs="", req_headers=None,
263 req_body=None, resp_body=None,
264 caller_name=None, extra=None):
265 if 'X-Auth-Token' in req_headers:
266 req_headers['X-Auth-Token'] = '<omitted>'
267 log_fmt = """Request (%s): %s %s %s%s
268 Request - Headers: %s
269 Body: %s
270 Response - Headers: %s
271 Body: %s"""
272
273 self.LOG.debug(
274 log_fmt % (
275 caller_name,
276 resp['status'],
277 method,
278 req_url,
279 secs,
280 str(req_headers),
Ken'ichi Ohmichie9140bf2014-12-10 05:31:16 +0000281 self._safe_body(req_body),
Sean Dague4f8d7022014-09-25 10:27:13 -0400282 str(resp),
Ken'ichi Ohmichie9140bf2014-12-10 05:31:16 +0000283 self._safe_body(resp_body)),
Sean Dague4f8d7022014-09-25 10:27:13 -0400284 extra=extra)
285
Sean Daguec522c092014-03-24 10:43:22 -0400286 def _log_request(self, method, req_url, resp,
Ghanshyam2a180b82014-06-16 13:54:22 +0900287 secs="", req_headers=None,
Sean Daguec522c092014-03-24 10:43:22 -0400288 req_body=None, resp_body=None):
Ghanshyam2a180b82014-06-16 13:54:22 +0900289 if req_headers is None:
290 req_headers = {}
Sean Dague0cc47572014-03-20 07:34:05 -0400291 # if we have the request id, put it in the right part of the log
Sean Dague89a85912014-03-19 16:37:29 -0400292 extra = dict(request_id=self._get_request_id(resp))
Sean Dague0cc47572014-03-20 07:34:05 -0400293 # NOTE(sdague): while we still have 6 callers to this function
294 # we're going to just provide work around on who is actually
295 # providing timings by gracefully adding no content if they don't.
296 # Once we're down to 1 caller, clean this up.
Matt Riedemann7efa5c32014-05-02 13:35:44 -0700297 caller_name = misc_utils.find_test_caller()
Sean Dague0cc47572014-03-20 07:34:05 -0400298 if secs:
299 secs = " %.3fs" % secs
Joe Gordon75d2e622014-10-14 12:31:22 -0700300 if not self.LOG.isEnabledFor(real_logging.DEBUG):
301 self.LOG.info(
302 'Request (%s): %s %s %s%s' % (
303 caller_name,
304 resp['status'],
305 method,
306 req_url,
307 secs),
308 extra=extra)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600309
Sean Dague4f8d7022014-09-25 10:27:13 -0400310 # Also look everything at DEBUG if you want to filter this
311 # out, don't run at debug.
312 self._log_request_full(method, req_url, resp, secs, req_headers,
313 req_body, resp_body, caller_name, extra)
Sean Daguec522c092014-03-24 10:43:22 -0400314
Dan Smithba6cb162012-08-14 07:22:42 -0700315 def _parse_resp(self, body):
Sean Daguefc072542014-11-24 11:50:25 -0500316 body = json.loads(body)
vponomaryov67b58fe2014-02-06 19:05:41 +0200317
Sean Daguefc072542014-11-24 11:50:25 -0500318 # We assume, that if the first value of the deserialized body's
319 # item set is a dict or a list, that we just return the first value
320 # of deserialized body.
321 # Essentially "cutting out" the first placeholder element in a body
322 # that looks like this:
323 #
324 # {
325 # "users": [
326 # ...
327 # ]
328 # }
329 try:
330 # Ensure there are not more than one top-level keys
331 if len(body.keys()) > 1:
332 return body
333 # Just return the "wrapped" element
334 first_key, first_item = body.items()[0]
335 if isinstance(first_item, (dict, list)):
336 return first_item
337 except (ValueError, IndexError):
338 pass
339 return body
Dan Smithba6cb162012-08-14 07:22:42 -0700340
Yaroslav Lobankovaede3802014-04-23 17:18:53 +0400341 def response_checker(self, method, resp, resp_body):
Attila Fazekas836e4782013-01-29 15:40:13 +0100342 if (resp.status in set((204, 205, 304)) or resp.status < 200 or
Pavel Sedláke267eba2013-04-03 15:56:36 +0200343 method.upper() == 'HEAD') and resp_body:
Attila Fazekas836e4782013-01-29 15:40:13 +0100344 raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
Attila Fazekasc3a095b2013-08-17 09:15:44 +0200345 # NOTE(afazekas):
Attila Fazekas836e4782013-01-29 15:40:13 +0100346 # If the HTTP Status Code is 205
347 # 'The response MUST NOT include an entity.'
348 # A HTTP entity has an entity-body and an 'entity-header'.
349 # In the HTTP response specification (Section 6) the 'entity-header'
350 # 'generic-header' and 'response-header' are in OR relation.
351 # All headers not in the above two group are considered as entity
352 # header in every interpretation.
353
354 if (resp.status == 205 and
355 0 != len(set(resp.keys()) - set(('status',)) -
356 self.response_header_lc - self.general_header_lc)):
357 raise exceptions.ResponseWithEntity()
Attila Fazekasc3a095b2013-08-17 09:15:44 +0200358 # NOTE(afazekas)
Attila Fazekas836e4782013-01-29 15:40:13 +0100359 # Now the swift sometimes (delete not empty container)
360 # returns with non json error response, we can create new rest class
361 # for swift.
362 # Usually RFC2616 says error responses SHOULD contain an explanation.
363 # The warning is normal for SHOULD/SHOULD NOT case
364
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100365 # Likely it will cause an error
Sean Daguec9a94f92014-06-23 08:31:50 -0400366 if method != 'HEAD' and not resp_body and resp.status >= 400:
Attila Fazekas11d2a772013-01-29 17:46:52 +0100367 self.LOG.warning("status >= 400 response with empty body")
Attila Fazekas836e4782013-01-29 15:40:13 +0100368
vponomaryov67b58fe2014-02-06 19:05:41 +0200369 def _request(self, method, url, headers=None, body=None):
Daryl Wallecke5b83d42011-11-10 14:39:02 -0600370 """A simple HTTP request interface."""
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000371 # Authenticate the request with the auth provider
372 req_url, req_headers, req_body = self.auth_provider.auth_request(
373 method, url, headers, body, self.filters)
Sean Dague89a85912014-03-19 16:37:29 -0400374
Sean Dague0cc47572014-03-20 07:34:05 -0400375 # Do the actual request, and time it
376 start = time.time()
Sean Dague2cb56992014-05-29 08:17:42 -0400377 self._log_request_start(method, req_url)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000378 resp, resp_body = self.http_obj.request(
379 req_url, method, headers=req_headers, body=req_body)
Sean Dague0cc47572014-03-20 07:34:05 -0400380 end = time.time()
Sean Daguec522c092014-03-24 10:43:22 -0400381 self._log_request(method, req_url, resp, secs=(end - start),
382 req_headers=req_headers, req_body=req_body,
383 resp_body=resp_body)
384
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000385 # Verify HTTP response codes
Yaroslav Lobankovaede3802014-04-23 17:18:53 +0400386 self.response_checker(method, resp, resp_body)
Attila Fazekas72c7a5f2012-12-03 17:17:23 +0100387
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100388 return resp, resp_body
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500389
Sergey Murashov4fccd322014-03-22 09:58:52 +0400390 def request(self, method, url, extra_headers=False, headers=None,
391 body=None):
392 # if extra_headers is True
393 # default headers would be added to headers
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100394 retry = 0
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100395
396 if headers is None:
vponomaryov67b58fe2014-02-06 19:05:41 +0200397 # NOTE(vponomaryov): if some client do not need headers,
398 # it should explicitly pass empty dict
399 headers = self.get_headers()
Sergey Murashov4fccd322014-03-22 09:58:52 +0400400 elif extra_headers:
401 try:
402 headers = headers.copy()
403 headers.update(self.get_headers())
404 except (ValueError, TypeError):
405 headers = self.get_headers()
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100406
407 resp, resp_body = self._request(method, url,
408 headers=headers, body=body)
409
410 while (resp.status == 413 and
411 'retry-after' in resp and
412 not self.is_absolute_limit(
413 resp, self._parse_resp(resp_body)) and
414 retry < MAX_RECURSION_DEPTH):
415 retry += 1
416 delay = int(resp['retry-after'])
417 time.sleep(delay)
418 resp, resp_body = self._request(method, url,
419 headers=headers, body=body)
420 self._error_checker(method, url, headers, body,
421 resp, resp_body)
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500422 return resp, resp_body
423
424 def _error_checker(self, method, url,
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100425 headers, body, resp, resp_body):
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500426
427 # NOTE(mtreinish): Check for httplib response from glance_http. The
428 # object can't be used here because importing httplib breaks httplib2.
429 # If another object from a class not imported were passed here as
430 # resp this could possibly fail
431 if str(type(resp)) == "<type 'instance'>":
432 ctype = resp.getheader('content-type')
433 else:
434 try:
435 ctype = resp['content-type']
436 # NOTE(mtreinish): Keystone delete user responses doesn't have a
437 # content-type header. (They don't have a body) So just pretend it
438 # is set.
439 except KeyError:
440 ctype = 'application/json'
441
Attila Fazekase72b7cd2013-03-26 18:34:21 +0100442 # It is not an error response
443 if resp.status < 400:
444 return
445
Sergey Murashovc10cca52014-01-16 12:48:47 +0400446 JSON_ENC = ['application/json', 'application/json; charset=utf-8']
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500447 # NOTE(mtreinish): This is for compatibility with Glance and swift
448 # APIs. These are the return content types that Glance api v1
449 # (and occasionally swift) are using.
Sergey Murashovc10cca52014-01-16 12:48:47 +0400450 TXT_ENC = ['text/plain', 'text/html', 'text/html; charset=utf-8',
451 'text/plain; charset=utf-8']
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500452
Ken'ichi Ohmichi938e3332014-12-10 14:09:18 +0000453 if ctype.lower() in JSON_ENC:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500454 parse_resp = True
Sergey Murashovc10cca52014-01-16 12:48:47 +0400455 elif ctype.lower() in TXT_ENC:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500456 parse_resp = False
457 else:
vponomaryov6cb6d192014-03-07 09:39:05 +0200458 raise exceptions.InvalidContentType(str(resp.status))
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500459
Rohit Karajgi6b1e1542012-05-14 05:55:54 -0700460 if resp.status == 401 or resp.status == 403:
Christian Schwede285a8482014-04-09 06:12:55 +0000461 raise exceptions.Unauthorized(resp_body)
Jay Pipes5135bfc2012-01-05 15:46:49 -0500462
463 if resp.status == 404:
Daryl Walleck8a707db2012-01-25 00:46:24 -0600464 raise exceptions.NotFound(resp_body)
Jay Pipes5135bfc2012-01-05 15:46:49 -0500465
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600466 if resp.status == 400:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500467 if parse_resp:
468 resp_body = self._parse_resp(resp_body)
David Kranz28e35c52012-07-10 10:14:38 -0400469 raise exceptions.BadRequest(resp_body)
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600470
David Kranz5a23d862012-02-14 09:48:55 -0500471 if resp.status == 409:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500472 if parse_resp:
473 resp_body = self._parse_resp(resp_body)
Anju5c3e510c2013-10-18 06:40:29 +0530474 raise exceptions.Conflict(resp_body)
David Kranz5a23d862012-02-14 09:48:55 -0500475
Daryl Wallecked8bef32011-12-05 23:02:08 -0600476 if resp.status == 413:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500477 if parse_resp:
478 resp_body = self._parse_resp(resp_body)
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100479 if self.is_absolute_limit(resp, resp_body):
480 raise exceptions.OverLimit(resp_body)
481 else:
482 raise exceptions.RateLimitExceeded(resp_body)
Brian Lamar12d9b292011-12-08 12:41:21 -0500483
ghanshyama5904002015-01-05 17:38:56 +0900484 if resp.status == 415:
485 if parse_resp:
486 resp_body = self._parse_resp(resp_body)
487 raise exceptions.InvalidContentType(resp_body)
488
Wangpana9b54c62013-02-28 11:04:32 +0800489 if resp.status == 422:
490 if parse_resp:
491 resp_body = self._parse_resp(resp_body)
492 raise exceptions.UnprocessableEntity(resp_body)
493
Daryl Wallecked8bef32011-12-05 23:02:08 -0600494 if resp.status in (500, 501):
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500495 message = resp_body
496 if parse_resp:
Rohan Kanade433994a2013-12-05 22:34:07 +0530497 try:
498 resp_body = self._parse_resp(resp_body)
499 except ValueError:
500 # If response body is a non-json string message.
501 # Use resp_body as is and raise InvalidResponseBody
502 # exception.
503 raise exceptions.InvalidHTTPResponseBody(message)
504 else:
vponomaryov6cb6d192014-03-07 09:39:05 +0200505 if isinstance(resp_body, dict):
506 # I'm seeing both computeFault
507 # and cloudServersFault come back.
508 # Will file a bug to fix, but leave as is for now.
509 if 'cloudServersFault' in resp_body:
510 message = resp_body['cloudServersFault']['message']
511 elif 'computeFault' in resp_body:
512 message = resp_body['computeFault']['message']
Ken'ichi Ohmichi43a694a2014-12-11 05:29:02 +0000513 elif 'error' in resp_body:
vponomaryov6cb6d192014-03-07 09:39:05 +0200514 message = resp_body['error']['message']
vponomaryov6cb6d192014-03-07 09:39:05 +0200515 elif 'message' in resp_body:
516 message = resp_body['message']
517 else:
518 message = resp_body
Dan Princea4b709c2012-10-10 12:27:59 -0400519
Ken'ichi Ohmichi43a694a2014-12-11 05:29:02 +0000520 if resp.status == 501:
521 raise exceptions.NotImplemented(message)
522 else:
523 raise exceptions.ServerFault(message)
Daryl Wallecked8bef32011-12-05 23:02:08 -0600524
David Kranz5a23d862012-02-14 09:48:55 -0500525 if resp.status >= 400:
vponomaryov6cb6d192014-03-07 09:39:05 +0200526 raise exceptions.UnexpectedResponseCode(str(resp.status))
David Kranz5a23d862012-02-14 09:48:55 -0500527
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100528 def is_absolute_limit(self, resp, resp_body):
529 if (not isinstance(resp_body, collections.Mapping) or
Pavel Sedláke267eba2013-04-03 15:56:36 +0200530 'retry-after' not in resp):
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100531 return True
Ken'ichi Ohmichi938e3332014-12-10 14:09:18 +0000532 over_limit = resp_body.get('overLimit', None)
533 if not over_limit:
534 return True
535 return 'exceed' in over_limit.get('message', 'blabla')
rajalakshmi-ganesan0275a0d2013-01-11 18:26:05 +0530536
David Kranz6aceb4a2012-06-05 14:05:45 -0400537 def wait_for_resource_deletion(self, id):
Sean Daguef237ccb2013-01-04 15:19:14 -0500538 """Waits for a resource to be deleted."""
David Kranz6aceb4a2012-06-05 14:05:45 -0400539 start_time = int(time.time())
540 while True:
541 if self.is_resource_deleted(id):
542 return
543 if int(time.time()) - start_time >= self.build_timeout:
Matt Riedemannd2b96512014-10-13 10:18:16 -0700544 message = ('Failed to delete %(resource_type)s %(id)s within '
545 'the required time (%(timeout)s s).' %
546 {'resource_type': self.resource_type, 'id': id,
547 'timeout': self.build_timeout})
Matt Riedemann30276742014-09-10 11:29:49 -0700548 caller = misc_utils.find_test_caller()
549 if caller:
550 message = '(%s) %s' % (caller, message)
551 raise exceptions.TimeoutException(message)
David Kranz6aceb4a2012-06-05 14:05:45 -0400552 time.sleep(self.build_interval)
553
554 def is_resource_deleted(self, id):
555 """
556 Subclasses override with specific deletion detection.
557 """
Attila Fazekasd236b4e2013-01-26 00:44:12 +0100558 message = ('"%s" does not implement is_resource_deleted'
559 % self.__class__.__name__)
560 raise NotImplementedError(message)
Dan Smithba6cb162012-08-14 07:22:42 -0700561
Matt Riedemannd2b96512014-10-13 10:18:16 -0700562 @property
563 def resource_type(self):
564 """Returns the primary type of resource this client works with."""
565 return 'resource'
566
Chris Yeohc266b282014-03-13 18:19:00 +1030567 @classmethod
568 def validate_response(cls, schema, resp, body):
569 # Only check the response if the status code is a success code
570 # TODO(cyeoh): Eventually we should be able to verify that a failure
571 # code if it exists is something that we expect. This is explicitly
572 # declared in the V3 API and so we should be able to export this in
573 # the response schema. For now we'll ignore it.
Ken'ichi Ohmichi4e0917c2014-03-19 15:33:47 +0900574 if resp.status in HTTP_SUCCESS:
Matthew Treinish2b2483e2014-05-08 23:26:10 -0400575 cls.expected_success(schema['status_code'], resp.status)
Ken'ichi Ohmichi57b384b2014-03-28 13:58:20 +0900576
577 # Check the body of a response
578 body_schema = schema.get('response_body')
579 if body_schema:
Chris Yeohc266b282014-03-13 18:19:00 +1030580 try:
Ken'ichi Ohmichi57b384b2014-03-28 13:58:20 +0900581 jsonschema.validate(body, body_schema)
Chris Yeohc266b282014-03-13 18:19:00 +1030582 except jsonschema.ValidationError as ex:
583 msg = ("HTTP response body is invalid (%s)") % ex
584 raise exceptions.InvalidHTTPResponseBody(msg)
585 else:
586 if body:
587 msg = ("HTTP response body should not exist (%s)") % body
588 raise exceptions.InvalidHTTPResponseBody(msg)
589
Ken'ichi Ohmichi57b384b2014-03-28 13:58:20 +0900590 # Check the header of a response
591 header_schema = schema.get('response_header')
592 if header_schema:
593 try:
594 jsonschema.validate(resp, header_schema)
595 except jsonschema.ValidationError as ex:
596 msg = ("HTTP response header is invalid (%s)") % ex
597 raise exceptions.InvalidHTTPResponseHeader(msg)