blob: ca87a75c5fce8bc84cb6db8527e7368d5e621d61 [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 Ohmichi0690ea42015-01-02 07:03:51 +000081 def __init__(self, auth_provider, service, endpoint_type='publicURL',
Ken'ichi Ohmichi0cd316b2014-12-24 03:51:04 +000082 build_interval=1, build_timeout=60):
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000083 self.auth_provider = auth_provider
Ken'ichi Ohmichi0cd316b2014-12-24 03:51:04 +000084 self.service = service
Ken'ichi Ohmichi0690ea42015-01-02 07:03:51 +000085 self.endpoint_type = endpoint_type
Ken'ichi Ohmichi0cd316b2014-12-24 03:51:04 +000086 self.build_interval = build_interval
87 self.build_timeout = build_timeout
chris fattarsi5098fa22012-04-17 13:27:00 -070088
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000089 # The version of the API this client implements
90 self.api_version = None
91 self._skip_path = False
Attila Fazekas72c7a5f2012-12-03 17:17:23 +010092 self.general_header_lc = set(('cache-control', 'connection',
93 'date', 'pragma', 'trailer',
94 'transfer-encoding', 'via',
95 'warning'))
96 self.response_header_lc = set(('accept-ranges', 'age', 'etag',
97 'location', 'proxy-authenticate',
98 'retry-after', 'server',
99 'vary', 'www-authenticate'))
Matthew Treinish684d8992014-01-30 16:27:40 +0000100 dscv = CONF.identity.disable_ssl_certificate_validation
Rob Crittendena7db6692014-11-23 18:44:38 -0500101 ca_certs = CONF.identity.ca_certificates_file
Mate Lakat23a58a32013-08-23 02:06:22 +0100102 self.http_obj = http.ClosingHttp(
Rob Crittendena7db6692014-11-23 18:44:38 -0500103 disable_ssl_certificate_validation=dscv, ca_certs=ca_certs)
chris fattarsi5098fa22012-04-17 13:27:00 -0700104
vponomaryov67b58fe2014-02-06 19:05:41 +0200105 def _get_type(self):
106 return self.TYPE
107
108 def get_headers(self, accept_type=None, send_type=None):
vponomaryov67b58fe2014-02-06 19:05:41 +0200109 if accept_type is None:
110 accept_type = self._get_type()
111 if send_type is None:
112 send_type = self._get_type()
113 return {'Content-Type': 'application/%s' % send_type,
114 'Accept': 'application/%s' % accept_type}
115
DennyZhang7be75002013-09-19 06:55:11 -0500116 def __str__(self):
117 STRING_LIMIT = 80
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000118 str_format = ("config:%s, service:%s, base_url:%s, "
119 "filters: %s, build_interval:%s, build_timeout:%s"
DennyZhang7be75002013-09-19 06:55:11 -0500120 "\ntoken:%s..., \nheaders:%s...")
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000121 return str_format % (CONF, self.service, self.base_url,
122 self.filters, self.build_interval,
123 self.build_timeout,
DennyZhang7be75002013-09-19 06:55:11 -0500124 str(self.token)[0:STRING_LIMIT],
vponomaryov67b58fe2014-02-06 19:05:41 +0200125 str(self.get_headers())[0:STRING_LIMIT])
DennyZhang7be75002013-09-19 06:55:11 -0500126
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000127 def _get_region(self, service):
chris fattarsi5098fa22012-04-17 13:27:00 -0700128 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000129 Returns the region for a specific service
chris fattarsi5098fa22012-04-17 13:27:00 -0700130 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000131 service_region = None
132 for cfgname in dir(CONF._config):
133 # Find all config.FOO.catalog_type and assume FOO is a service.
134 cfg = getattr(CONF, cfgname)
135 catalog_type = getattr(cfg, 'catalog_type', None)
136 if catalog_type == service:
137 service_region = getattr(cfg, 'region', None)
138 if not service_region:
139 service_region = CONF.identity.region
140 return service_region
chris fattarsi5098fa22012-04-17 13:27:00 -0700141
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000142 @property
143 def user(self):
Andrea Frittoli86ad28d2014-03-20 10:09:12 +0000144 return self.auth_provider.credentials.username
Li Ma216550f2013-06-12 11:26:08 -0700145
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000146 @property
Andrea Frittoli9612e812014-03-13 10:57:26 +0000147 def user_id(self):
148 return self.auth_provider.credentials.user_id
149
150 @property
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000151 def tenant_name(self):
Andrea Frittoli86ad28d2014-03-20 10:09:12 +0000152 return self.auth_provider.credentials.tenant_name
153
154 @property
155 def tenant_id(self):
156 return self.auth_provider.credentials.tenant_id
chris fattarsi5098fa22012-04-17 13:27:00 -0700157
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000158 @property
159 def password(self):
Andrea Frittoli86ad28d2014-03-20 10:09:12 +0000160 return self.auth_provider.credentials.password
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000161
162 @property
163 def base_url(self):
164 return self.auth_provider.base_url(filters=self.filters)
165
166 @property
Andrea Frittoli77f9da42014-02-06 11:18:19 +0000167 def token(self):
168 return self.auth_provider.get_token()
169
170 @property
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000171 def filters(self):
172 _filters = dict(
173 service=self.service,
Ken'ichi Ohmichi0690ea42015-01-02 07:03:51 +0000174 endpoint_type=self.endpoint_type,
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000175 region=self._get_region(self.service)
176 )
177 if self.api_version is not None:
178 _filters['api_version'] = self.api_version
179 if self._skip_path:
180 _filters['skip_path'] = self._skip_path
181 return _filters
182
183 def skip_path(self):
chris fattarsi5098fa22012-04-17 13:27:00 -0700184 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000185 When set, ignore the path part of the base URL from the catalog
chris fattarsi5098fa22012-04-17 13:27:00 -0700186 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000187 self._skip_path = True
chris fattarsi5098fa22012-04-17 13:27:00 -0700188
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000189 def reset_path(self):
Attila Fazekasb2902af2013-02-16 16:22:44 +0100190 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000191 When reset, use the base URL from the catalog as-is
Daryl Walleck1465d612011-11-02 02:22:15 -0500192 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000193 self._skip_path = False
Brant Knudsonc7ca3342013-03-28 21:08:50 -0500194
Matthew Treinish2b2483e2014-05-08 23:26:10 -0400195 @classmethod
196 def expected_success(cls, expected_code, read_code):
Attila Fazekas54a42862013-07-28 22:31:06 +0200197 assert_msg = ("This function only allowed to use for HTTP status"
Ghanshyam25ad2c32014-07-17 16:44:55 +0900198 "codes which explicitly defined in the RFC 7231 & 4918."
199 "{0} is not a defined Success Code!"
200 ).format(expected_code)
Matthew Treinish2b2483e2014-05-08 23:26:10 -0400201 if isinstance(expected_code, list):
202 for code in expected_code:
203 assert code in HTTP_SUCCESS, assert_msg
204 else:
205 assert expected_code in HTTP_SUCCESS, assert_msg
Attila Fazekas54a42862013-07-28 22:31:06 +0200206
207 # NOTE(afazekas): the http status code above 400 is processed by
208 # the _error_checker method
Matthew Treinish2b2483e2014-05-08 23:26:10 -0400209 if read_code < 400:
210 pattern = """Unexpected http success status code {0},
211 The expected status code is {1}"""
212 if ((not isinstance(expected_code, list) and
Matthew Treinish1d14c542014-06-17 20:25:40 -0400213 (read_code != expected_code)) or
214 (isinstance(expected_code, list) and
215 (read_code not in expected_code))):
Attila Fazekas54a42862013-07-28 22:31:06 +0200216 details = pattern.format(read_code, expected_code)
217 raise exceptions.InvalidHttpSuccessCode(details)
218
Sergey Murashov4fccd322014-03-22 09:58:52 +0400219 def post(self, url, body, headers=None, extra_headers=False):
220 return self.request('POST', url, extra_headers, headers, body)
Daryl Walleck1465d612011-11-02 02:22:15 -0500221
Sergey Murashov4fccd322014-03-22 09:58:52 +0400222 def get(self, url, headers=None, extra_headers=False):
223 return self.request('GET', url, extra_headers, headers)
Daryl Walleck1465d612011-11-02 02:22:15 -0500224
Sergey Murashov4fccd322014-03-22 09:58:52 +0400225 def delete(self, url, headers=None, body=None, extra_headers=False):
226 return self.request('DELETE', url, extra_headers, headers, body)
Daryl Walleck1465d612011-11-02 02:22:15 -0500227
Sergey Murashov4fccd322014-03-22 09:58:52 +0400228 def patch(self, url, body, headers=None, extra_headers=False):
229 return self.request('PATCH', url, extra_headers, headers, body)
rajalakshmi-ganesanab426722013-02-08 15:49:15 +0530230
Sergey Murashov4fccd322014-03-22 09:58:52 +0400231 def put(self, url, body, headers=None, extra_headers=False):
232 return self.request('PUT', url, extra_headers, headers, body)
Daryl Walleck1465d612011-11-02 02:22:15 -0500233
Sergey Murashov4fccd322014-03-22 09:58:52 +0400234 def head(self, url, headers=None, extra_headers=False):
235 return self.request('HEAD', url, extra_headers, headers)
Larisa Ustalov6c3c7802012-11-05 12:25:19 +0200236
Sergey Murashov4fccd322014-03-22 09:58:52 +0400237 def copy(self, url, headers=None, extra_headers=False):
238 return self.request('COPY', url, extra_headers, headers)
dwalleck5d734432012-10-04 01:11:47 -0500239
Matthew Treinishc0f768f2013-03-11 14:24:16 -0400240 def get_versions(self):
241 resp, body = self.get('')
242 body = self._parse_resp(body)
Matthew Treinishc0f768f2013-03-11 14:24:16 -0400243 versions = map(lambda x: x['id'], body)
244 return resp, versions
245
Sean Dague89a85912014-03-19 16:37:29 -0400246 def _get_request_id(self, resp):
247 for i in ('x-openstack-request-id', 'x-compute-request-id'):
248 if i in resp:
249 return resp[i]
250 return ""
Attila Fazekas11d2a772013-01-29 17:46:52 +0100251
Ken'ichi Ohmichie9140bf2014-12-10 05:31:16 +0000252 def _safe_body(self, body, maxlen=4096):
253 # convert a structure into a string safely
254 try:
255 text = six.text_type(body)
256 except UnicodeDecodeError:
257 # if this isn't actually text, return marker that
258 return "<BinaryData: removed>"
259 if len(text) > maxlen:
260 return text[:maxlen]
261 else:
262 return text
263
Ghanshyam2a180b82014-06-16 13:54:22 +0900264 def _log_request_start(self, method, req_url, req_headers=None,
Sean Dague2cb56992014-05-29 08:17:42 -0400265 req_body=None):
Ghanshyam2a180b82014-06-16 13:54:22 +0900266 if req_headers is None:
267 req_headers = {}
Sean Dague2cb56992014-05-29 08:17:42 -0400268 caller_name = misc_utils.find_test_caller()
269 trace_regex = CONF.debug.trace_requests
270 if trace_regex and re.search(trace_regex, caller_name):
271 self.LOG.debug('Starting Request (%s): %s %s' %
272 (caller_name, method, req_url))
273
Sean Dague4f8d7022014-09-25 10:27:13 -0400274 def _log_request_full(self, method, req_url, resp,
275 secs="", req_headers=None,
276 req_body=None, resp_body=None,
277 caller_name=None, extra=None):
278 if 'X-Auth-Token' in req_headers:
279 req_headers['X-Auth-Token'] = '<omitted>'
280 log_fmt = """Request (%s): %s %s %s%s
281 Request - Headers: %s
282 Body: %s
283 Response - Headers: %s
284 Body: %s"""
285
286 self.LOG.debug(
287 log_fmt % (
288 caller_name,
289 resp['status'],
290 method,
291 req_url,
292 secs,
293 str(req_headers),
Ken'ichi Ohmichie9140bf2014-12-10 05:31:16 +0000294 self._safe_body(req_body),
Sean Dague4f8d7022014-09-25 10:27:13 -0400295 str(resp),
Ken'ichi Ohmichie9140bf2014-12-10 05:31:16 +0000296 self._safe_body(resp_body)),
Sean Dague4f8d7022014-09-25 10:27:13 -0400297 extra=extra)
298
Sean Daguec522c092014-03-24 10:43:22 -0400299 def _log_request(self, method, req_url, resp,
Ghanshyam2a180b82014-06-16 13:54:22 +0900300 secs="", req_headers=None,
Sean Daguec522c092014-03-24 10:43:22 -0400301 req_body=None, resp_body=None):
Ghanshyam2a180b82014-06-16 13:54:22 +0900302 if req_headers is None:
303 req_headers = {}
Sean Dague0cc47572014-03-20 07:34:05 -0400304 # if we have the request id, put it in the right part of the log
Sean Dague89a85912014-03-19 16:37:29 -0400305 extra = dict(request_id=self._get_request_id(resp))
Sean Dague0cc47572014-03-20 07:34:05 -0400306 # NOTE(sdague): while we still have 6 callers to this function
307 # we're going to just provide work around on who is actually
308 # providing timings by gracefully adding no content if they don't.
309 # Once we're down to 1 caller, clean this up.
Matt Riedemann7efa5c32014-05-02 13:35:44 -0700310 caller_name = misc_utils.find_test_caller()
Sean Dague0cc47572014-03-20 07:34:05 -0400311 if secs:
312 secs = " %.3fs" % secs
Joe Gordon75d2e622014-10-14 12:31:22 -0700313 if not self.LOG.isEnabledFor(real_logging.DEBUG):
314 self.LOG.info(
315 'Request (%s): %s %s %s%s' % (
316 caller_name,
317 resp['status'],
318 method,
319 req_url,
320 secs),
321 extra=extra)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600322
Sean Dague4f8d7022014-09-25 10:27:13 -0400323 # Also look everything at DEBUG if you want to filter this
324 # out, don't run at debug.
325 self._log_request_full(method, req_url, resp, secs, req_headers,
326 req_body, resp_body, caller_name, extra)
Sean Daguec522c092014-03-24 10:43:22 -0400327
Dan Smithba6cb162012-08-14 07:22:42 -0700328 def _parse_resp(self, body):
Sean Daguefc072542014-11-24 11:50:25 -0500329 body = json.loads(body)
vponomaryov67b58fe2014-02-06 19:05:41 +0200330
Sean Daguefc072542014-11-24 11:50:25 -0500331 # We assume, that if the first value of the deserialized body's
332 # item set is a dict or a list, that we just return the first value
333 # of deserialized body.
334 # Essentially "cutting out" the first placeholder element in a body
335 # that looks like this:
336 #
337 # {
338 # "users": [
339 # ...
340 # ]
341 # }
342 try:
343 # Ensure there are not more than one top-level keys
344 if len(body.keys()) > 1:
345 return body
346 # Just return the "wrapped" element
347 first_key, first_item = body.items()[0]
348 if isinstance(first_item, (dict, list)):
349 return first_item
350 except (ValueError, IndexError):
351 pass
352 return body
Dan Smithba6cb162012-08-14 07:22:42 -0700353
Yaroslav Lobankovaede3802014-04-23 17:18:53 +0400354 def response_checker(self, method, resp, resp_body):
Attila Fazekas836e4782013-01-29 15:40:13 +0100355 if (resp.status in set((204, 205, 304)) or resp.status < 200 or
Pavel Sedláke267eba2013-04-03 15:56:36 +0200356 method.upper() == 'HEAD') and resp_body:
Attila Fazekas836e4782013-01-29 15:40:13 +0100357 raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
Attila Fazekasc3a095b2013-08-17 09:15:44 +0200358 # NOTE(afazekas):
Attila Fazekas836e4782013-01-29 15:40:13 +0100359 # If the HTTP Status Code is 205
360 # 'The response MUST NOT include an entity.'
361 # A HTTP entity has an entity-body and an 'entity-header'.
362 # In the HTTP response specification (Section 6) the 'entity-header'
363 # 'generic-header' and 'response-header' are in OR relation.
364 # All headers not in the above two group are considered as entity
365 # header in every interpretation.
366
367 if (resp.status == 205 and
368 0 != len(set(resp.keys()) - set(('status',)) -
369 self.response_header_lc - self.general_header_lc)):
370 raise exceptions.ResponseWithEntity()
Attila Fazekasc3a095b2013-08-17 09:15:44 +0200371 # NOTE(afazekas)
Attila Fazekas836e4782013-01-29 15:40:13 +0100372 # Now the swift sometimes (delete not empty container)
373 # returns with non json error response, we can create new rest class
374 # for swift.
375 # Usually RFC2616 says error responses SHOULD contain an explanation.
376 # The warning is normal for SHOULD/SHOULD NOT case
377
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100378 # Likely it will cause an error
Sean Daguec9a94f92014-06-23 08:31:50 -0400379 if method != 'HEAD' and not resp_body and resp.status >= 400:
Attila Fazekas11d2a772013-01-29 17:46:52 +0100380 self.LOG.warning("status >= 400 response with empty body")
Attila Fazekas836e4782013-01-29 15:40:13 +0100381
vponomaryov67b58fe2014-02-06 19:05:41 +0200382 def _request(self, method, url, headers=None, body=None):
Daryl Wallecke5b83d42011-11-10 14:39:02 -0600383 """A simple HTTP request interface."""
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000384 # Authenticate the request with the auth provider
385 req_url, req_headers, req_body = self.auth_provider.auth_request(
386 method, url, headers, body, self.filters)
Sean Dague89a85912014-03-19 16:37:29 -0400387
Sean Dague0cc47572014-03-20 07:34:05 -0400388 # Do the actual request, and time it
389 start = time.time()
Sean Dague2cb56992014-05-29 08:17:42 -0400390 self._log_request_start(method, req_url)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000391 resp, resp_body = self.http_obj.request(
392 req_url, method, headers=req_headers, body=req_body)
Sean Dague0cc47572014-03-20 07:34:05 -0400393 end = time.time()
Sean Daguec522c092014-03-24 10:43:22 -0400394 self._log_request(method, req_url, resp, secs=(end - start),
395 req_headers=req_headers, req_body=req_body,
396 resp_body=resp_body)
397
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000398 # Verify HTTP response codes
Yaroslav Lobankovaede3802014-04-23 17:18:53 +0400399 self.response_checker(method, resp, resp_body)
Attila Fazekas72c7a5f2012-12-03 17:17:23 +0100400
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100401 return resp, resp_body
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500402
Sergey Murashov4fccd322014-03-22 09:58:52 +0400403 def request(self, method, url, extra_headers=False, headers=None,
404 body=None):
405 # if extra_headers is True
406 # default headers would be added to headers
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100407 retry = 0
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100408
409 if headers is None:
vponomaryov67b58fe2014-02-06 19:05:41 +0200410 # NOTE(vponomaryov): if some client do not need headers,
411 # it should explicitly pass empty dict
412 headers = self.get_headers()
Sergey Murashov4fccd322014-03-22 09:58:52 +0400413 elif extra_headers:
414 try:
415 headers = headers.copy()
416 headers.update(self.get_headers())
417 except (ValueError, TypeError):
418 headers = self.get_headers()
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100419
420 resp, resp_body = self._request(method, url,
421 headers=headers, body=body)
422
423 while (resp.status == 413 and
424 'retry-after' in resp and
425 not self.is_absolute_limit(
426 resp, self._parse_resp(resp_body)) and
427 retry < MAX_RECURSION_DEPTH):
428 retry += 1
429 delay = int(resp['retry-after'])
430 time.sleep(delay)
431 resp, resp_body = self._request(method, url,
432 headers=headers, body=body)
433 self._error_checker(method, url, headers, body,
434 resp, resp_body)
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500435 return resp, resp_body
436
437 def _error_checker(self, method, url,
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100438 headers, body, resp, resp_body):
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500439
440 # NOTE(mtreinish): Check for httplib response from glance_http. The
441 # object can't be used here because importing httplib breaks httplib2.
442 # If another object from a class not imported were passed here as
443 # resp this could possibly fail
444 if str(type(resp)) == "<type 'instance'>":
445 ctype = resp.getheader('content-type')
446 else:
447 try:
448 ctype = resp['content-type']
449 # NOTE(mtreinish): Keystone delete user responses doesn't have a
450 # content-type header. (They don't have a body) So just pretend it
451 # is set.
452 except KeyError:
453 ctype = 'application/json'
454
Attila Fazekase72b7cd2013-03-26 18:34:21 +0100455 # It is not an error response
456 if resp.status < 400:
457 return
458
Sergey Murashovc10cca52014-01-16 12:48:47 +0400459 JSON_ENC = ['application/json', 'application/json; charset=utf-8']
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500460 # NOTE(mtreinish): This is for compatibility with Glance and swift
461 # APIs. These are the return content types that Glance api v1
462 # (and occasionally swift) are using.
Sergey Murashovc10cca52014-01-16 12:48:47 +0400463 TXT_ENC = ['text/plain', 'text/html', 'text/html; charset=utf-8',
464 'text/plain; charset=utf-8']
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500465
Ken'ichi Ohmichi938e3332014-12-10 14:09:18 +0000466 if ctype.lower() in JSON_ENC:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500467 parse_resp = True
Sergey Murashovc10cca52014-01-16 12:48:47 +0400468 elif ctype.lower() in TXT_ENC:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500469 parse_resp = False
470 else:
vponomaryov6cb6d192014-03-07 09:39:05 +0200471 raise exceptions.InvalidContentType(str(resp.status))
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500472
Rohit Karajgi6b1e1542012-05-14 05:55:54 -0700473 if resp.status == 401 or resp.status == 403:
Christian Schwede285a8482014-04-09 06:12:55 +0000474 raise exceptions.Unauthorized(resp_body)
Jay Pipes5135bfc2012-01-05 15:46:49 -0500475
476 if resp.status == 404:
Daryl Walleck8a707db2012-01-25 00:46:24 -0600477 raise exceptions.NotFound(resp_body)
Jay Pipes5135bfc2012-01-05 15:46:49 -0500478
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600479 if resp.status == 400:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500480 if parse_resp:
481 resp_body = self._parse_resp(resp_body)
David Kranz28e35c52012-07-10 10:14:38 -0400482 raise exceptions.BadRequest(resp_body)
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600483
David Kranz5a23d862012-02-14 09:48:55 -0500484 if resp.status == 409:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500485 if parse_resp:
486 resp_body = self._parse_resp(resp_body)
Anju5c3e510c2013-10-18 06:40:29 +0530487 raise exceptions.Conflict(resp_body)
David Kranz5a23d862012-02-14 09:48:55 -0500488
Daryl Wallecked8bef32011-12-05 23:02:08 -0600489 if resp.status == 413:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500490 if parse_resp:
491 resp_body = self._parse_resp(resp_body)
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100492 if self.is_absolute_limit(resp, resp_body):
493 raise exceptions.OverLimit(resp_body)
494 else:
495 raise exceptions.RateLimitExceeded(resp_body)
Brian Lamar12d9b292011-12-08 12:41:21 -0500496
Wangpana9b54c62013-02-28 11:04:32 +0800497 if resp.status == 422:
498 if parse_resp:
499 resp_body = self._parse_resp(resp_body)
500 raise exceptions.UnprocessableEntity(resp_body)
501
Daryl Wallecked8bef32011-12-05 23:02:08 -0600502 if resp.status in (500, 501):
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500503 message = resp_body
504 if parse_resp:
Rohan Kanade433994a2013-12-05 22:34:07 +0530505 try:
506 resp_body = self._parse_resp(resp_body)
507 except ValueError:
508 # If response body is a non-json string message.
509 # Use resp_body as is and raise InvalidResponseBody
510 # exception.
511 raise exceptions.InvalidHTTPResponseBody(message)
512 else:
vponomaryov6cb6d192014-03-07 09:39:05 +0200513 if isinstance(resp_body, dict):
514 # I'm seeing both computeFault
515 # and cloudServersFault come back.
516 # Will file a bug to fix, but leave as is for now.
517 if 'cloudServersFault' in resp_body:
518 message = resp_body['cloudServersFault']['message']
519 elif 'computeFault' in resp_body:
520 message = resp_body['computeFault']['message']
Ken'ichi Ohmichi43a694a2014-12-11 05:29:02 +0000521 elif 'error' in resp_body:
vponomaryov6cb6d192014-03-07 09:39:05 +0200522 message = resp_body['error']['message']
vponomaryov6cb6d192014-03-07 09:39:05 +0200523 elif 'message' in resp_body:
524 message = resp_body['message']
525 else:
526 message = resp_body
Dan Princea4b709c2012-10-10 12:27:59 -0400527
Ken'ichi Ohmichi43a694a2014-12-11 05:29:02 +0000528 if resp.status == 501:
529 raise exceptions.NotImplemented(message)
530 else:
531 raise exceptions.ServerFault(message)
Daryl Wallecked8bef32011-12-05 23:02:08 -0600532
David Kranz5a23d862012-02-14 09:48:55 -0500533 if resp.status >= 400:
vponomaryov6cb6d192014-03-07 09:39:05 +0200534 raise exceptions.UnexpectedResponseCode(str(resp.status))
David Kranz5a23d862012-02-14 09:48:55 -0500535
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100536 def is_absolute_limit(self, resp, resp_body):
537 if (not isinstance(resp_body, collections.Mapping) or
Pavel Sedláke267eba2013-04-03 15:56:36 +0200538 'retry-after' not in resp):
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100539 return True
Ken'ichi Ohmichi938e3332014-12-10 14:09:18 +0000540 over_limit = resp_body.get('overLimit', None)
541 if not over_limit:
542 return True
543 return 'exceed' in over_limit.get('message', 'blabla')
rajalakshmi-ganesan0275a0d2013-01-11 18:26:05 +0530544
David Kranz6aceb4a2012-06-05 14:05:45 -0400545 def wait_for_resource_deletion(self, id):
Sean Daguef237ccb2013-01-04 15:19:14 -0500546 """Waits for a resource to be deleted."""
David Kranz6aceb4a2012-06-05 14:05:45 -0400547 start_time = int(time.time())
548 while True:
549 if self.is_resource_deleted(id):
550 return
551 if int(time.time()) - start_time >= self.build_timeout:
Matt Riedemannd2b96512014-10-13 10:18:16 -0700552 message = ('Failed to delete %(resource_type)s %(id)s within '
553 'the required time (%(timeout)s s).' %
554 {'resource_type': self.resource_type, 'id': id,
555 'timeout': self.build_timeout})
Matt Riedemann30276742014-09-10 11:29:49 -0700556 caller = misc_utils.find_test_caller()
557 if caller:
558 message = '(%s) %s' % (caller, message)
559 raise exceptions.TimeoutException(message)
David Kranz6aceb4a2012-06-05 14:05:45 -0400560 time.sleep(self.build_interval)
561
562 def is_resource_deleted(self, id):
563 """
564 Subclasses override with specific deletion detection.
565 """
Attila Fazekasd236b4e2013-01-26 00:44:12 +0100566 message = ('"%s" does not implement is_resource_deleted'
567 % self.__class__.__name__)
568 raise NotImplementedError(message)
Dan Smithba6cb162012-08-14 07:22:42 -0700569
Matt Riedemannd2b96512014-10-13 10:18:16 -0700570 @property
571 def resource_type(self):
572 """Returns the primary type of resource this client works with."""
573 return 'resource'
574
Chris Yeohc266b282014-03-13 18:19:00 +1030575 @classmethod
576 def validate_response(cls, schema, resp, body):
577 # Only check the response if the status code is a success code
578 # TODO(cyeoh): Eventually we should be able to verify that a failure
579 # code if it exists is something that we expect. This is explicitly
580 # declared in the V3 API and so we should be able to export this in
581 # the response schema. For now we'll ignore it.
Ken'ichi Ohmichi4e0917c2014-03-19 15:33:47 +0900582 if resp.status in HTTP_SUCCESS:
Matthew Treinish2b2483e2014-05-08 23:26:10 -0400583 cls.expected_success(schema['status_code'], resp.status)
Ken'ichi Ohmichi57b384b2014-03-28 13:58:20 +0900584
585 # Check the body of a response
586 body_schema = schema.get('response_body')
587 if body_schema:
Chris Yeohc266b282014-03-13 18:19:00 +1030588 try:
Ken'ichi Ohmichi57b384b2014-03-28 13:58:20 +0900589 jsonschema.validate(body, body_schema)
Chris Yeohc266b282014-03-13 18:19:00 +1030590 except jsonschema.ValidationError as ex:
591 msg = ("HTTP response body is invalid (%s)") % ex
592 raise exceptions.InvalidHTTPResponseBody(msg)
593 else:
594 if body:
595 msg = ("HTTP response body should not exist (%s)") % body
596 raise exceptions.InvalidHTTPResponseBody(msg)
597
Ken'ichi Ohmichi57b384b2014-03-28 13:58:20 +0900598 # Check the header of a response
599 header_schema = schema.get('response_header')
600 if header_schema:
601 try:
602 jsonschema.validate(resp, header_schema)
603 except jsonschema.ValidationError as ex:
604 msg = ("HTTP response header is invalid (%s)") % ex
605 raise exceptions.InvalidHTTPResponseHeader(msg)
606
Dan Smithba6cb162012-08-14 07:22:42 -0700607
Marc Koderer24eb89c2014-01-31 11:23:33 +0100608class NegativeRestClient(RestClient):
609 """
610 Version of RestClient that does not raise exceptions.
611 """
612 def _error_checker(self, method, url,
613 headers, body, resp, resp_body):
614 pass
615
616 def send_request(self, method, url_template, resources, body=None):
617 url = url_template % tuple(resources)
618 if method == "GET":
619 resp, body = self.get(url)
620 elif method == "POST":
vponomaryov67b58fe2014-02-06 19:05:41 +0200621 resp, body = self.post(url, body)
Marc Koderer24eb89c2014-01-31 11:23:33 +0100622 elif method == "PUT":
vponomaryov67b58fe2014-02-06 19:05:41 +0200623 resp, body = self.put(url, body)
Marc Koderer24eb89c2014-01-31 11:23:33 +0100624 elif method == "PATCH":
vponomaryov67b58fe2014-02-06 19:05:41 +0200625 resp, body = self.patch(url, body)
Marc Koderer24eb89c2014-01-31 11:23:33 +0100626 elif method == "HEAD":
627 resp, body = self.head(url)
628 elif method == "DELETE":
629 resp, body = self.delete(url)
630 elif method == "COPY":
631 resp, body = self.copy(url)
632 else:
633 assert False
634
635 return resp, body