blob: 576fc2687a875400c2c30f68334d3d8313cf6e1d [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
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000081 def __init__(self, auth_provider):
82 self.auth_provider = auth_provider
chris fattarsi5098fa22012-04-17 13:27:00 -070083
JordanP5d29b2c2013-12-18 13:56:03 +000084 self.endpoint_url = None
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000085 self.service = None
86 # The version of the API this client implements
87 self.api_version = None
88 self._skip_path = False
Matthew Treinish684d8992014-01-30 16:27:40 +000089 self.build_interval = CONF.compute.build_interval
90 self.build_timeout = CONF.compute.build_timeout
Attila Fazekas72c7a5f2012-12-03 17:17:23 +010091 self.general_header_lc = set(('cache-control', 'connection',
92 'date', 'pragma', 'trailer',
93 'transfer-encoding', 'via',
94 'warning'))
95 self.response_header_lc = set(('accept-ranges', 'age', 'etag',
96 'location', 'proxy-authenticate',
97 'retry-after', 'server',
98 'vary', 'www-authenticate'))
Matthew Treinish684d8992014-01-30 16:27:40 +000099 dscv = CONF.identity.disable_ssl_certificate_validation
Rob Crittendena7db6692014-11-23 18:44:38 -0500100 ca_certs = CONF.identity.ca_certificates_file
Mate Lakat23a58a32013-08-23 02:06:22 +0100101 self.http_obj = http.ClosingHttp(
Rob Crittendena7db6692014-11-23 18:44:38 -0500102 disable_ssl_certificate_validation=dscv, ca_certs=ca_certs)
chris fattarsi5098fa22012-04-17 13:27:00 -0700103
vponomaryov67b58fe2014-02-06 19:05:41 +0200104 def _get_type(self):
105 return self.TYPE
106
107 def get_headers(self, accept_type=None, send_type=None):
vponomaryov67b58fe2014-02-06 19:05:41 +0200108 if accept_type is None:
109 accept_type = self._get_type()
110 if send_type is None:
111 send_type = self._get_type()
112 return {'Content-Type': 'application/%s' % send_type,
113 'Accept': 'application/%s' % accept_type}
114
DennyZhang7be75002013-09-19 06:55:11 -0500115 def __str__(self):
116 STRING_LIMIT = 80
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000117 str_format = ("config:%s, service:%s, base_url:%s, "
118 "filters: %s, build_interval:%s, build_timeout:%s"
DennyZhang7be75002013-09-19 06:55:11 -0500119 "\ntoken:%s..., \nheaders:%s...")
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000120 return str_format % (CONF, self.service, self.base_url,
121 self.filters, self.build_interval,
122 self.build_timeout,
DennyZhang7be75002013-09-19 06:55:11 -0500123 str(self.token)[0:STRING_LIMIT],
vponomaryov67b58fe2014-02-06 19:05:41 +0200124 str(self.get_headers())[0:STRING_LIMIT])
DennyZhang7be75002013-09-19 06:55:11 -0500125
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000126 def _get_region(self, service):
chris fattarsi5098fa22012-04-17 13:27:00 -0700127 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000128 Returns the region for a specific service
chris fattarsi5098fa22012-04-17 13:27:00 -0700129 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000130 service_region = None
131 for cfgname in dir(CONF._config):
132 # Find all config.FOO.catalog_type and assume FOO is a service.
133 cfg = getattr(CONF, cfgname)
134 catalog_type = getattr(cfg, 'catalog_type', None)
135 if catalog_type == service:
136 service_region = getattr(cfg, 'region', None)
137 if not service_region:
138 service_region = CONF.identity.region
139 return service_region
chris fattarsi5098fa22012-04-17 13:27:00 -0700140
JordanP5d29b2c2013-12-18 13:56:03 +0000141 def _get_endpoint_type(self, service):
142 """
143 Returns the endpoint type for a specific service
144 """
145 # If the client requests a specific endpoint type, then be it
146 if self.endpoint_url:
147 return self.endpoint_url
148 endpoint_type = None
149 for cfgname in dir(CONF._config):
150 # Find all config.FOO.catalog_type and assume FOO is a service.
151 cfg = getattr(CONF, cfgname)
152 catalog_type = getattr(cfg, 'catalog_type', None)
153 if catalog_type == service:
154 endpoint_type = getattr(cfg, 'endpoint_type', 'publicURL')
155 break
156 # Special case for compute v3 service which hasn't its own
157 # configuration group
158 else:
159 if service == CONF.compute.catalog_v3_type:
160 endpoint_type = CONF.compute.endpoint_type
161 return endpoint_type
162
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000163 @property
164 def user(self):
Andrea Frittoli86ad28d2014-03-20 10:09:12 +0000165 return self.auth_provider.credentials.username
Li Ma216550f2013-06-12 11:26:08 -0700166
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000167 @property
Andrea Frittoli9612e812014-03-13 10:57:26 +0000168 def user_id(self):
169 return self.auth_provider.credentials.user_id
170
171 @property
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000172 def tenant_name(self):
Andrea Frittoli86ad28d2014-03-20 10:09:12 +0000173 return self.auth_provider.credentials.tenant_name
174
175 @property
176 def tenant_id(self):
177 return self.auth_provider.credentials.tenant_id
chris fattarsi5098fa22012-04-17 13:27:00 -0700178
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000179 @property
180 def password(self):
Andrea Frittoli86ad28d2014-03-20 10:09:12 +0000181 return self.auth_provider.credentials.password
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000182
183 @property
184 def base_url(self):
185 return self.auth_provider.base_url(filters=self.filters)
186
187 @property
Andrea Frittoli77f9da42014-02-06 11:18:19 +0000188 def token(self):
189 return self.auth_provider.get_token()
190
191 @property
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000192 def filters(self):
193 _filters = dict(
194 service=self.service,
JordanP5d29b2c2013-12-18 13:56:03 +0000195 endpoint_type=self._get_endpoint_type(self.service),
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000196 region=self._get_region(self.service)
197 )
198 if self.api_version is not None:
199 _filters['api_version'] = self.api_version
200 if self._skip_path:
201 _filters['skip_path'] = self._skip_path
202 return _filters
203
204 def skip_path(self):
chris fattarsi5098fa22012-04-17 13:27:00 -0700205 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000206 When set, ignore the path part of the base URL from the catalog
chris fattarsi5098fa22012-04-17 13:27:00 -0700207 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000208 self._skip_path = True
chris fattarsi5098fa22012-04-17 13:27:00 -0700209
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000210 def reset_path(self):
Attila Fazekasb2902af2013-02-16 16:22:44 +0100211 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000212 When reset, use the base URL from the catalog as-is
Daryl Walleck1465d612011-11-02 02:22:15 -0500213 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000214 self._skip_path = False
Brant Knudsonc7ca3342013-03-28 21:08:50 -0500215
Matthew Treinish2b2483e2014-05-08 23:26:10 -0400216 @classmethod
217 def expected_success(cls, expected_code, read_code):
Attila Fazekas54a42862013-07-28 22:31:06 +0200218 assert_msg = ("This function only allowed to use for HTTP status"
Ghanshyam25ad2c32014-07-17 16:44:55 +0900219 "codes which explicitly defined in the RFC 7231 & 4918."
220 "{0} is not a defined Success Code!"
221 ).format(expected_code)
Matthew Treinish2b2483e2014-05-08 23:26:10 -0400222 if isinstance(expected_code, list):
223 for code in expected_code:
224 assert code in HTTP_SUCCESS, assert_msg
225 else:
226 assert expected_code in HTTP_SUCCESS, assert_msg
Attila Fazekas54a42862013-07-28 22:31:06 +0200227
228 # NOTE(afazekas): the http status code above 400 is processed by
229 # the _error_checker method
Matthew Treinish2b2483e2014-05-08 23:26:10 -0400230 if read_code < 400:
231 pattern = """Unexpected http success status code {0},
232 The expected status code is {1}"""
233 if ((not isinstance(expected_code, list) and
Matthew Treinish1d14c542014-06-17 20:25:40 -0400234 (read_code != expected_code)) or
235 (isinstance(expected_code, list) and
236 (read_code not in expected_code))):
Attila Fazekas54a42862013-07-28 22:31:06 +0200237 details = pattern.format(read_code, expected_code)
238 raise exceptions.InvalidHttpSuccessCode(details)
239
Sergey Murashov4fccd322014-03-22 09:58:52 +0400240 def post(self, url, body, headers=None, extra_headers=False):
241 return self.request('POST', url, extra_headers, headers, body)
Daryl Walleck1465d612011-11-02 02:22:15 -0500242
Sergey Murashov4fccd322014-03-22 09:58:52 +0400243 def get(self, url, headers=None, extra_headers=False):
244 return self.request('GET', url, extra_headers, headers)
Daryl Walleck1465d612011-11-02 02:22:15 -0500245
Sergey Murashov4fccd322014-03-22 09:58:52 +0400246 def delete(self, url, headers=None, body=None, extra_headers=False):
247 return self.request('DELETE', url, extra_headers, headers, body)
Daryl Walleck1465d612011-11-02 02:22:15 -0500248
Sergey Murashov4fccd322014-03-22 09:58:52 +0400249 def patch(self, url, body, headers=None, extra_headers=False):
250 return self.request('PATCH', url, extra_headers, headers, body)
rajalakshmi-ganesanab426722013-02-08 15:49:15 +0530251
Sergey Murashov4fccd322014-03-22 09:58:52 +0400252 def put(self, url, body, headers=None, extra_headers=False):
253 return self.request('PUT', url, extra_headers, headers, body)
Daryl Walleck1465d612011-11-02 02:22:15 -0500254
Sergey Murashov4fccd322014-03-22 09:58:52 +0400255 def head(self, url, headers=None, extra_headers=False):
256 return self.request('HEAD', url, extra_headers, headers)
Larisa Ustalov6c3c7802012-11-05 12:25:19 +0200257
Sergey Murashov4fccd322014-03-22 09:58:52 +0400258 def copy(self, url, headers=None, extra_headers=False):
259 return self.request('COPY', url, extra_headers, headers)
dwalleck5d734432012-10-04 01:11:47 -0500260
Matthew Treinishc0f768f2013-03-11 14:24:16 -0400261 def get_versions(self):
262 resp, body = self.get('')
263 body = self._parse_resp(body)
Matthew Treinishc0f768f2013-03-11 14:24:16 -0400264 versions = map(lambda x: x['id'], body)
265 return resp, versions
266
Sean Dague89a85912014-03-19 16:37:29 -0400267 def _get_request_id(self, resp):
268 for i in ('x-openstack-request-id', 'x-compute-request-id'):
269 if i in resp:
270 return resp[i]
271 return ""
Attila Fazekas11d2a772013-01-29 17:46:52 +0100272
Ken'ichi Ohmichie9140bf2014-12-10 05:31:16 +0000273 def _safe_body(self, body, maxlen=4096):
274 # convert a structure into a string safely
275 try:
276 text = six.text_type(body)
277 except UnicodeDecodeError:
278 # if this isn't actually text, return marker that
279 return "<BinaryData: removed>"
280 if len(text) > maxlen:
281 return text[:maxlen]
282 else:
283 return text
284
Ghanshyam2a180b82014-06-16 13:54:22 +0900285 def _log_request_start(self, method, req_url, req_headers=None,
Sean Dague2cb56992014-05-29 08:17:42 -0400286 req_body=None):
Ghanshyam2a180b82014-06-16 13:54:22 +0900287 if req_headers is None:
288 req_headers = {}
Sean Dague2cb56992014-05-29 08:17:42 -0400289 caller_name = misc_utils.find_test_caller()
290 trace_regex = CONF.debug.trace_requests
291 if trace_regex and re.search(trace_regex, caller_name):
292 self.LOG.debug('Starting Request (%s): %s %s' %
293 (caller_name, method, req_url))
294
Sean Dague4f8d7022014-09-25 10:27:13 -0400295 def _log_request_full(self, method, req_url, resp,
296 secs="", req_headers=None,
297 req_body=None, resp_body=None,
298 caller_name=None, extra=None):
299 if 'X-Auth-Token' in req_headers:
300 req_headers['X-Auth-Token'] = '<omitted>'
301 log_fmt = """Request (%s): %s %s %s%s
302 Request - Headers: %s
303 Body: %s
304 Response - Headers: %s
305 Body: %s"""
306
307 self.LOG.debug(
308 log_fmt % (
309 caller_name,
310 resp['status'],
311 method,
312 req_url,
313 secs,
314 str(req_headers),
Ken'ichi Ohmichie9140bf2014-12-10 05:31:16 +0000315 self._safe_body(req_body),
Sean Dague4f8d7022014-09-25 10:27:13 -0400316 str(resp),
Ken'ichi Ohmichie9140bf2014-12-10 05:31:16 +0000317 self._safe_body(resp_body)),
Sean Dague4f8d7022014-09-25 10:27:13 -0400318 extra=extra)
319
Sean Daguec522c092014-03-24 10:43:22 -0400320 def _log_request(self, method, req_url, resp,
Ghanshyam2a180b82014-06-16 13:54:22 +0900321 secs="", req_headers=None,
Sean Daguec522c092014-03-24 10:43:22 -0400322 req_body=None, resp_body=None):
Ghanshyam2a180b82014-06-16 13:54:22 +0900323 if req_headers is None:
324 req_headers = {}
Sean Dague0cc47572014-03-20 07:34:05 -0400325 # if we have the request id, put it in the right part of the log
Sean Dague89a85912014-03-19 16:37:29 -0400326 extra = dict(request_id=self._get_request_id(resp))
Sean Dague0cc47572014-03-20 07:34:05 -0400327 # NOTE(sdague): while we still have 6 callers to this function
328 # we're going to just provide work around on who is actually
329 # providing timings by gracefully adding no content if they don't.
330 # Once we're down to 1 caller, clean this up.
Matt Riedemann7efa5c32014-05-02 13:35:44 -0700331 caller_name = misc_utils.find_test_caller()
Sean Dague0cc47572014-03-20 07:34:05 -0400332 if secs:
333 secs = " %.3fs" % secs
Joe Gordon75d2e622014-10-14 12:31:22 -0700334 if not self.LOG.isEnabledFor(real_logging.DEBUG):
335 self.LOG.info(
336 'Request (%s): %s %s %s%s' % (
337 caller_name,
338 resp['status'],
339 method,
340 req_url,
341 secs),
342 extra=extra)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600343
Sean Dague4f8d7022014-09-25 10:27:13 -0400344 # Also look everything at DEBUG if you want to filter this
345 # out, don't run at debug.
346 self._log_request_full(method, req_url, resp, secs, req_headers,
347 req_body, resp_body, caller_name, extra)
Sean Daguec522c092014-03-24 10:43:22 -0400348
Dan Smithba6cb162012-08-14 07:22:42 -0700349 def _parse_resp(self, body):
Sean Daguefc072542014-11-24 11:50:25 -0500350 body = json.loads(body)
vponomaryov67b58fe2014-02-06 19:05:41 +0200351
Sean Daguefc072542014-11-24 11:50:25 -0500352 # We assume, that if the first value of the deserialized body's
353 # item set is a dict or a list, that we just return the first value
354 # of deserialized body.
355 # Essentially "cutting out" the first placeholder element in a body
356 # that looks like this:
357 #
358 # {
359 # "users": [
360 # ...
361 # ]
362 # }
363 try:
364 # Ensure there are not more than one top-level keys
365 if len(body.keys()) > 1:
366 return body
367 # Just return the "wrapped" element
368 first_key, first_item = body.items()[0]
369 if isinstance(first_item, (dict, list)):
370 return first_item
371 except (ValueError, IndexError):
372 pass
373 return body
Dan Smithba6cb162012-08-14 07:22:42 -0700374
Yaroslav Lobankovaede3802014-04-23 17:18:53 +0400375 def response_checker(self, method, resp, resp_body):
Attila Fazekas836e4782013-01-29 15:40:13 +0100376 if (resp.status in set((204, 205, 304)) or resp.status < 200 or
Pavel Sedláke267eba2013-04-03 15:56:36 +0200377 method.upper() == 'HEAD') and resp_body:
Attila Fazekas836e4782013-01-29 15:40:13 +0100378 raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
Attila Fazekasc3a095b2013-08-17 09:15:44 +0200379 # NOTE(afazekas):
Attila Fazekas836e4782013-01-29 15:40:13 +0100380 # If the HTTP Status Code is 205
381 # 'The response MUST NOT include an entity.'
382 # A HTTP entity has an entity-body and an 'entity-header'.
383 # In the HTTP response specification (Section 6) the 'entity-header'
384 # 'generic-header' and 'response-header' are in OR relation.
385 # All headers not in the above two group are considered as entity
386 # header in every interpretation.
387
388 if (resp.status == 205 and
389 0 != len(set(resp.keys()) - set(('status',)) -
390 self.response_header_lc - self.general_header_lc)):
391 raise exceptions.ResponseWithEntity()
Attila Fazekasc3a095b2013-08-17 09:15:44 +0200392 # NOTE(afazekas)
Attila Fazekas836e4782013-01-29 15:40:13 +0100393 # Now the swift sometimes (delete not empty container)
394 # returns with non json error response, we can create new rest class
395 # for swift.
396 # Usually RFC2616 says error responses SHOULD contain an explanation.
397 # The warning is normal for SHOULD/SHOULD NOT case
398
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100399 # Likely it will cause an error
Sean Daguec9a94f92014-06-23 08:31:50 -0400400 if method != 'HEAD' and not resp_body and resp.status >= 400:
Attila Fazekas11d2a772013-01-29 17:46:52 +0100401 self.LOG.warning("status >= 400 response with empty body")
Attila Fazekas836e4782013-01-29 15:40:13 +0100402
vponomaryov67b58fe2014-02-06 19:05:41 +0200403 def _request(self, method, url, headers=None, body=None):
Daryl Wallecke5b83d42011-11-10 14:39:02 -0600404 """A simple HTTP request interface."""
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000405 # Authenticate the request with the auth provider
406 req_url, req_headers, req_body = self.auth_provider.auth_request(
407 method, url, headers, body, self.filters)
Sean Dague89a85912014-03-19 16:37:29 -0400408
Sean Dague0cc47572014-03-20 07:34:05 -0400409 # Do the actual request, and time it
410 start = time.time()
Sean Dague2cb56992014-05-29 08:17:42 -0400411 self._log_request_start(method, req_url)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000412 resp, resp_body = self.http_obj.request(
413 req_url, method, headers=req_headers, body=req_body)
Sean Dague0cc47572014-03-20 07:34:05 -0400414 end = time.time()
Sean Daguec522c092014-03-24 10:43:22 -0400415 self._log_request(method, req_url, resp, secs=(end - start),
416 req_headers=req_headers, req_body=req_body,
417 resp_body=resp_body)
418
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000419 # Verify HTTP response codes
Yaroslav Lobankovaede3802014-04-23 17:18:53 +0400420 self.response_checker(method, resp, resp_body)
Attila Fazekas72c7a5f2012-12-03 17:17:23 +0100421
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100422 return resp, resp_body
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500423
Sergey Murashov4fccd322014-03-22 09:58:52 +0400424 def request(self, method, url, extra_headers=False, headers=None,
425 body=None):
426 # if extra_headers is True
427 # default headers would be added to headers
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100428 retry = 0
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100429
430 if headers is None:
vponomaryov67b58fe2014-02-06 19:05:41 +0200431 # NOTE(vponomaryov): if some client do not need headers,
432 # it should explicitly pass empty dict
433 headers = self.get_headers()
Sergey Murashov4fccd322014-03-22 09:58:52 +0400434 elif extra_headers:
435 try:
436 headers = headers.copy()
437 headers.update(self.get_headers())
438 except (ValueError, TypeError):
439 headers = self.get_headers()
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100440
441 resp, resp_body = self._request(method, url,
442 headers=headers, body=body)
443
444 while (resp.status == 413 and
445 'retry-after' in resp and
446 not self.is_absolute_limit(
447 resp, self._parse_resp(resp_body)) and
448 retry < MAX_RECURSION_DEPTH):
449 retry += 1
450 delay = int(resp['retry-after'])
451 time.sleep(delay)
452 resp, resp_body = self._request(method, url,
453 headers=headers, body=body)
454 self._error_checker(method, url, headers, body,
455 resp, resp_body)
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500456 return resp, resp_body
457
458 def _error_checker(self, method, url,
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100459 headers, body, resp, resp_body):
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500460
461 # NOTE(mtreinish): Check for httplib response from glance_http. The
462 # object can't be used here because importing httplib breaks httplib2.
463 # If another object from a class not imported were passed here as
464 # resp this could possibly fail
465 if str(type(resp)) == "<type 'instance'>":
466 ctype = resp.getheader('content-type')
467 else:
468 try:
469 ctype = resp['content-type']
470 # NOTE(mtreinish): Keystone delete user responses doesn't have a
471 # content-type header. (They don't have a body) So just pretend it
472 # is set.
473 except KeyError:
474 ctype = 'application/json'
475
Attila Fazekase72b7cd2013-03-26 18:34:21 +0100476 # It is not an error response
477 if resp.status < 400:
478 return
479
Sergey Murashovc10cca52014-01-16 12:48:47 +0400480 JSON_ENC = ['application/json', 'application/json; charset=utf-8']
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500481 # NOTE(mtreinish): This is for compatibility with Glance and swift
482 # APIs. These are the return content types that Glance api v1
483 # (and occasionally swift) are using.
Sergey Murashovc10cca52014-01-16 12:48:47 +0400484 TXT_ENC = ['text/plain', 'text/html', 'text/html; charset=utf-8',
485 'text/plain; charset=utf-8']
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500486
Ken'ichi Ohmichi938e3332014-12-10 14:09:18 +0000487 if ctype.lower() in JSON_ENC:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500488 parse_resp = True
Sergey Murashovc10cca52014-01-16 12:48:47 +0400489 elif ctype.lower() in TXT_ENC:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500490 parse_resp = False
491 else:
vponomaryov6cb6d192014-03-07 09:39:05 +0200492 raise exceptions.InvalidContentType(str(resp.status))
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500493
Rohit Karajgi6b1e1542012-05-14 05:55:54 -0700494 if resp.status == 401 or resp.status == 403:
Christian Schwede285a8482014-04-09 06:12:55 +0000495 raise exceptions.Unauthorized(resp_body)
Jay Pipes5135bfc2012-01-05 15:46:49 -0500496
497 if resp.status == 404:
Daryl Walleck8a707db2012-01-25 00:46:24 -0600498 raise exceptions.NotFound(resp_body)
Jay Pipes5135bfc2012-01-05 15:46:49 -0500499
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600500 if resp.status == 400:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500501 if parse_resp:
502 resp_body = self._parse_resp(resp_body)
David Kranz28e35c52012-07-10 10:14:38 -0400503 raise exceptions.BadRequest(resp_body)
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600504
David Kranz5a23d862012-02-14 09:48:55 -0500505 if resp.status == 409:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500506 if parse_resp:
507 resp_body = self._parse_resp(resp_body)
Anju5c3e510c2013-10-18 06:40:29 +0530508 raise exceptions.Conflict(resp_body)
David Kranz5a23d862012-02-14 09:48:55 -0500509
Daryl Wallecked8bef32011-12-05 23:02:08 -0600510 if resp.status == 413:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500511 if parse_resp:
512 resp_body = self._parse_resp(resp_body)
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100513 if self.is_absolute_limit(resp, resp_body):
514 raise exceptions.OverLimit(resp_body)
515 else:
516 raise exceptions.RateLimitExceeded(resp_body)
Brian Lamar12d9b292011-12-08 12:41:21 -0500517
Wangpana9b54c62013-02-28 11:04:32 +0800518 if resp.status == 422:
519 if parse_resp:
520 resp_body = self._parse_resp(resp_body)
521 raise exceptions.UnprocessableEntity(resp_body)
522
Daryl Wallecked8bef32011-12-05 23:02:08 -0600523 if resp.status in (500, 501):
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500524 message = resp_body
525 if parse_resp:
Rohan Kanade433994a2013-12-05 22:34:07 +0530526 try:
527 resp_body = self._parse_resp(resp_body)
528 except ValueError:
529 # If response body is a non-json string message.
530 # Use resp_body as is and raise InvalidResponseBody
531 # exception.
532 raise exceptions.InvalidHTTPResponseBody(message)
533 else:
vponomaryov6cb6d192014-03-07 09:39:05 +0200534 if isinstance(resp_body, dict):
535 # I'm seeing both computeFault
536 # and cloudServersFault come back.
537 # Will file a bug to fix, but leave as is for now.
538 if 'cloudServersFault' in resp_body:
539 message = resp_body['cloudServersFault']['message']
540 elif 'computeFault' in resp_body:
541 message = resp_body['computeFault']['message']
542 elif 'error' in resp_body: # Keystone errors
543 message = resp_body['error']['message']
544 raise exceptions.IdentityError(message)
545 elif 'message' in resp_body:
546 message = resp_body['message']
547 else:
548 message = resp_body
Dan Princea4b709c2012-10-10 12:27:59 -0400549
Anju5c3e510c2013-10-18 06:40:29 +0530550 raise exceptions.ServerFault(message)
Daryl Wallecked8bef32011-12-05 23:02:08 -0600551
David Kranz5a23d862012-02-14 09:48:55 -0500552 if resp.status >= 400:
vponomaryov6cb6d192014-03-07 09:39:05 +0200553 raise exceptions.UnexpectedResponseCode(str(resp.status))
David Kranz5a23d862012-02-14 09:48:55 -0500554
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100555 def is_absolute_limit(self, resp, resp_body):
556 if (not isinstance(resp_body, collections.Mapping) or
Pavel Sedláke267eba2013-04-03 15:56:36 +0200557 'retry-after' not in resp):
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100558 return True
Ken'ichi Ohmichi938e3332014-12-10 14:09:18 +0000559 over_limit = resp_body.get('overLimit', None)
560 if not over_limit:
561 return True
562 return 'exceed' in over_limit.get('message', 'blabla')
rajalakshmi-ganesan0275a0d2013-01-11 18:26:05 +0530563
David Kranz6aceb4a2012-06-05 14:05:45 -0400564 def wait_for_resource_deletion(self, id):
Sean Daguef237ccb2013-01-04 15:19:14 -0500565 """Waits for a resource to be deleted."""
David Kranz6aceb4a2012-06-05 14:05:45 -0400566 start_time = int(time.time())
567 while True:
568 if self.is_resource_deleted(id):
569 return
570 if int(time.time()) - start_time >= self.build_timeout:
Matt Riedemannd2b96512014-10-13 10:18:16 -0700571 message = ('Failed to delete %(resource_type)s %(id)s within '
572 'the required time (%(timeout)s s).' %
573 {'resource_type': self.resource_type, 'id': id,
574 'timeout': self.build_timeout})
Matt Riedemann30276742014-09-10 11:29:49 -0700575 caller = misc_utils.find_test_caller()
576 if caller:
577 message = '(%s) %s' % (caller, message)
578 raise exceptions.TimeoutException(message)
David Kranz6aceb4a2012-06-05 14:05:45 -0400579 time.sleep(self.build_interval)
580
581 def is_resource_deleted(self, id):
582 """
583 Subclasses override with specific deletion detection.
584 """
Attila Fazekasd236b4e2013-01-26 00:44:12 +0100585 message = ('"%s" does not implement is_resource_deleted'
586 % self.__class__.__name__)
587 raise NotImplementedError(message)
Dan Smithba6cb162012-08-14 07:22:42 -0700588
Matt Riedemannd2b96512014-10-13 10:18:16 -0700589 @property
590 def resource_type(self):
591 """Returns the primary type of resource this client works with."""
592 return 'resource'
593
Chris Yeohc266b282014-03-13 18:19:00 +1030594 @classmethod
595 def validate_response(cls, schema, resp, body):
596 # Only check the response if the status code is a success code
597 # TODO(cyeoh): Eventually we should be able to verify that a failure
598 # code if it exists is something that we expect. This is explicitly
599 # declared in the V3 API and so we should be able to export this in
600 # the response schema. For now we'll ignore it.
Ken'ichi Ohmichi4e0917c2014-03-19 15:33:47 +0900601 if resp.status in HTTP_SUCCESS:
Matthew Treinish2b2483e2014-05-08 23:26:10 -0400602 cls.expected_success(schema['status_code'], resp.status)
Ken'ichi Ohmichi57b384b2014-03-28 13:58:20 +0900603
604 # Check the body of a response
605 body_schema = schema.get('response_body')
606 if body_schema:
Chris Yeohc266b282014-03-13 18:19:00 +1030607 try:
Ken'ichi Ohmichi57b384b2014-03-28 13:58:20 +0900608 jsonschema.validate(body, body_schema)
Chris Yeohc266b282014-03-13 18:19:00 +1030609 except jsonschema.ValidationError as ex:
610 msg = ("HTTP response body is invalid (%s)") % ex
611 raise exceptions.InvalidHTTPResponseBody(msg)
612 else:
613 if body:
614 msg = ("HTTP response body should not exist (%s)") % body
615 raise exceptions.InvalidHTTPResponseBody(msg)
616
Ken'ichi Ohmichi57b384b2014-03-28 13:58:20 +0900617 # Check the header of a response
618 header_schema = schema.get('response_header')
619 if header_schema:
620 try:
621 jsonschema.validate(resp, header_schema)
622 except jsonschema.ValidationError as ex:
623 msg = ("HTTP response header is invalid (%s)") % ex
624 raise exceptions.InvalidHTTPResponseHeader(msg)
625
Dan Smithba6cb162012-08-14 07:22:42 -0700626
Marc Koderer24eb89c2014-01-31 11:23:33 +0100627class NegativeRestClient(RestClient):
628 """
629 Version of RestClient that does not raise exceptions.
630 """
631 def _error_checker(self, method, url,
632 headers, body, resp, resp_body):
633 pass
634
635 def send_request(self, method, url_template, resources, body=None):
636 url = url_template % tuple(resources)
637 if method == "GET":
638 resp, body = self.get(url)
639 elif method == "POST":
vponomaryov67b58fe2014-02-06 19:05:41 +0200640 resp, body = self.post(url, body)
Marc Koderer24eb89c2014-01-31 11:23:33 +0100641 elif method == "PUT":
vponomaryov67b58fe2014-02-06 19:05:41 +0200642 resp, body = self.put(url, body)
Marc Koderer24eb89c2014-01-31 11:23:33 +0100643 elif method == "PATCH":
vponomaryov67b58fe2014-02-06 19:05:41 +0200644 resp, body = self.patch(url, body)
Marc Koderer24eb89c2014-01-31 11:23:33 +0100645 elif method == "HEAD":
646 resp, body = self.head(url)
647 elif method == "DELETE":
648 resp, body = self.delete(url)
649 elif method == "COPY":
650 resp, body = self.copy(url)
651 else:
652 assert False
653
654 return resp, body