blob: 8c07d4f308d2483894b68caf4d84ed9164a81370 [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
Sean Dague99862622014-03-19 18:41:38 -040018import inspect
Matthew Treinisha83a16e2012-12-07 13:44:02 -050019import json
Dan Smithba6cb162012-08-14 07:22:42 -070020from lxml import etree
Attila Fazekas11d2a772013-01-29 17:46:52 +010021import re
Eoghan Glynna5598972012-03-01 09:27:17 -050022import time
Jay Pipes3f981df2012-03-27 18:59:44 -040023
Chris Yeohc266b282014-03-13 18:19:00 +103024import jsonschema
25
Mate Lakat23a58a32013-08-23 02:06:22 +010026from tempest.common import http
Matthew Treinish28f164c2014-03-04 18:55:06 +000027from tempest.common import xml_utils as common
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
Attila Fazekas11d2a772013-01-29 17:46:52 +010036TOKEN_CHARS_RE = re.compile('^[-A-Za-z0-9+/=]*$')
Eoghan Glynna5598972012-03-01 09:27:17 -050037
Attila Fazekas54a42862013-07-28 22:31:06 +020038# All the successful HTTP status codes from RFC 2616
39HTTP_SUCCESS = (200, 201, 202, 203, 204, 205, 206)
40
Eoghan Glynna5598972012-03-01 09:27:17 -050041
Daryl Walleck1465d612011-11-02 02:22:15 -050042class RestClient(object):
vponomaryov67b58fe2014-02-06 19:05:41 +020043
Dan Smithba6cb162012-08-14 07:22:42 -070044 TYPE = "json"
vponomaryov67b58fe2014-02-06 19:05:41 +020045
46 # This is used by _parse_resp method
47 # Redefine it for purposes of your xml service client
48 # List should contain top-xml_tag-names of data, which is like list/array
49 # For example, in keystone it is users, roles, tenants and services
50 # All of it has children with same tag-names
51 list_tags = []
52
53 # This is used by _parse_resp method too
54 # Used for selection of dict-like xmls,
55 # like metadata for Vms in nova, and volumes in cinder
56 dict_tags = ["metadata", ]
57
Attila Fazekas11d2a772013-01-29 17:46:52 +010058 LOG = logging.getLogger(__name__)
Daryl Walleck1465d612011-11-02 02:22:15 -050059
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000060 def __init__(self, auth_provider):
61 self.auth_provider = auth_provider
chris fattarsi5098fa22012-04-17 13:27:00 -070062
JordanP5d29b2c2013-12-18 13:56:03 +000063 self.endpoint_url = None
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000064 self.service = None
65 # The version of the API this client implements
66 self.api_version = None
67 self._skip_path = False
Matthew Treinish684d8992014-01-30 16:27:40 +000068 self.build_interval = CONF.compute.build_interval
69 self.build_timeout = CONF.compute.build_timeout
Attila Fazekas72c7a5f2012-12-03 17:17:23 +010070 self.general_header_lc = set(('cache-control', 'connection',
71 'date', 'pragma', 'trailer',
72 'transfer-encoding', 'via',
73 'warning'))
74 self.response_header_lc = set(('accept-ranges', 'age', 'etag',
75 'location', 'proxy-authenticate',
76 'retry-after', 'server',
77 'vary', 'www-authenticate'))
Matthew Treinish684d8992014-01-30 16:27:40 +000078 dscv = CONF.identity.disable_ssl_certificate_validation
Mate Lakat23a58a32013-08-23 02:06:22 +010079 self.http_obj = http.ClosingHttp(
80 disable_ssl_certificate_validation=dscv)
chris fattarsi5098fa22012-04-17 13:27:00 -070081
vponomaryov67b58fe2014-02-06 19:05:41 +020082 def _get_type(self):
83 return self.TYPE
84
85 def get_headers(self, accept_type=None, send_type=None):
vponomaryov67b58fe2014-02-06 19:05:41 +020086 if accept_type is None:
87 accept_type = self._get_type()
88 if send_type is None:
89 send_type = self._get_type()
90 return {'Content-Type': 'application/%s' % send_type,
91 'Accept': 'application/%s' % accept_type}
92
DennyZhang7be75002013-09-19 06:55:11 -050093 def __str__(self):
94 STRING_LIMIT = 80
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000095 str_format = ("config:%s, service:%s, base_url:%s, "
96 "filters: %s, build_interval:%s, build_timeout:%s"
DennyZhang7be75002013-09-19 06:55:11 -050097 "\ntoken:%s..., \nheaders:%s...")
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000098 return str_format % (CONF, self.service, self.base_url,
99 self.filters, self.build_interval,
100 self.build_timeout,
DennyZhang7be75002013-09-19 06:55:11 -0500101 str(self.token)[0:STRING_LIMIT],
vponomaryov67b58fe2014-02-06 19:05:41 +0200102 str(self.get_headers())[0:STRING_LIMIT])
DennyZhang7be75002013-09-19 06:55:11 -0500103
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000104 def _get_region(self, service):
chris fattarsi5098fa22012-04-17 13:27:00 -0700105 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000106 Returns the region for a specific service
chris fattarsi5098fa22012-04-17 13:27:00 -0700107 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000108 service_region = None
109 for cfgname in dir(CONF._config):
110 # Find all config.FOO.catalog_type and assume FOO is a service.
111 cfg = getattr(CONF, cfgname)
112 catalog_type = getattr(cfg, 'catalog_type', None)
113 if catalog_type == service:
114 service_region = getattr(cfg, 'region', None)
115 if not service_region:
116 service_region = CONF.identity.region
117 return service_region
chris fattarsi5098fa22012-04-17 13:27:00 -0700118
JordanP5d29b2c2013-12-18 13:56:03 +0000119 def _get_endpoint_type(self, service):
120 """
121 Returns the endpoint type for a specific service
122 """
123 # If the client requests a specific endpoint type, then be it
124 if self.endpoint_url:
125 return self.endpoint_url
126 endpoint_type = None
127 for cfgname in dir(CONF._config):
128 # Find all config.FOO.catalog_type and assume FOO is a service.
129 cfg = getattr(CONF, cfgname)
130 catalog_type = getattr(cfg, 'catalog_type', None)
131 if catalog_type == service:
132 endpoint_type = getattr(cfg, 'endpoint_type', 'publicURL')
133 break
134 # Special case for compute v3 service which hasn't its own
135 # configuration group
136 else:
137 if service == CONF.compute.catalog_v3_type:
138 endpoint_type = CONF.compute.endpoint_type
139 return endpoint_type
140
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000141 @property
142 def user(self):
143 return self.auth_provider.credentials.get('username', None)
Li Ma216550f2013-06-12 11:26:08 -0700144
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000145 @property
146 def tenant_name(self):
147 return self.auth_provider.credentials.get('tenant_name', None)
chris fattarsi5098fa22012-04-17 13:27:00 -0700148
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000149 @property
150 def password(self):
151 return self.auth_provider.credentials.get('password', None)
152
153 @property
154 def base_url(self):
155 return self.auth_provider.base_url(filters=self.filters)
156
157 @property
Andrea Frittoli77f9da42014-02-06 11:18:19 +0000158 def token(self):
159 return self.auth_provider.get_token()
160
161 @property
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000162 def filters(self):
163 _filters = dict(
164 service=self.service,
JordanP5d29b2c2013-12-18 13:56:03 +0000165 endpoint_type=self._get_endpoint_type(self.service),
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000166 region=self._get_region(self.service)
167 )
168 if self.api_version is not None:
169 _filters['api_version'] = self.api_version
170 if self._skip_path:
171 _filters['skip_path'] = self._skip_path
172 return _filters
173
174 def skip_path(self):
chris fattarsi5098fa22012-04-17 13:27:00 -0700175 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000176 When set, ignore the path part of the base URL from the catalog
chris fattarsi5098fa22012-04-17 13:27:00 -0700177 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000178 self._skip_path = True
chris fattarsi5098fa22012-04-17 13:27:00 -0700179
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000180 def reset_path(self):
Attila Fazekasb2902af2013-02-16 16:22:44 +0100181 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000182 When reset, use the base URL from the catalog as-is
Daryl Walleck1465d612011-11-02 02:22:15 -0500183 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000184 self._skip_path = False
Brant Knudsonc7ca3342013-03-28 21:08:50 -0500185
Attila Fazekas54a42862013-07-28 22:31:06 +0200186 def expected_success(self, expected_code, read_code):
187 assert_msg = ("This function only allowed to use for HTTP status"
188 "codes which explicitly defined in the RFC 2616. {0}"
189 " is not a defined Success Code!").format(expected_code)
190 assert expected_code in HTTP_SUCCESS, assert_msg
191
192 # NOTE(afazekas): the http status code above 400 is processed by
193 # the _error_checker method
194 if read_code < 400 and read_code != expected_code:
195 pattern = """Unexpected http success status code {0},
196 The expected status code is {1}"""
197 details = pattern.format(read_code, expected_code)
198 raise exceptions.InvalidHttpSuccessCode(details)
199
Sergey Murashov4fccd322014-03-22 09:58:52 +0400200 def post(self, url, body, headers=None, extra_headers=False):
201 return self.request('POST', url, extra_headers, headers, body)
Daryl Walleck1465d612011-11-02 02:22:15 -0500202
Sergey Murashov4fccd322014-03-22 09:58:52 +0400203 def get(self, url, headers=None, extra_headers=False):
204 return self.request('GET', url, extra_headers, headers)
Daryl Walleck1465d612011-11-02 02:22:15 -0500205
Sergey Murashov4fccd322014-03-22 09:58:52 +0400206 def delete(self, url, headers=None, body=None, extra_headers=False):
207 return self.request('DELETE', url, extra_headers, headers, body)
Daryl Walleck1465d612011-11-02 02:22:15 -0500208
Sergey Murashov4fccd322014-03-22 09:58:52 +0400209 def patch(self, url, body, headers=None, extra_headers=False):
210 return self.request('PATCH', url, extra_headers, headers, body)
rajalakshmi-ganesanab426722013-02-08 15:49:15 +0530211
Sergey Murashov4fccd322014-03-22 09:58:52 +0400212 def put(self, url, body, headers=None, extra_headers=False):
213 return self.request('PUT', url, extra_headers, headers, body)
Daryl Walleck1465d612011-11-02 02:22:15 -0500214
Sergey Murashov4fccd322014-03-22 09:58:52 +0400215 def head(self, url, headers=None, extra_headers=False):
216 return self.request('HEAD', url, extra_headers, headers)
Larisa Ustalov6c3c7802012-11-05 12:25:19 +0200217
Sergey Murashov4fccd322014-03-22 09:58:52 +0400218 def copy(self, url, headers=None, extra_headers=False):
219 return self.request('COPY', url, extra_headers, headers)
dwalleck5d734432012-10-04 01:11:47 -0500220
Matthew Treinishc0f768f2013-03-11 14:24:16 -0400221 def get_versions(self):
222 resp, body = self.get('')
223 body = self._parse_resp(body)
Matthew Treinishc0f768f2013-03-11 14:24:16 -0400224 versions = map(lambda x: x['id'], body)
225 return resp, versions
226
Sean Dague99862622014-03-19 18:41:38 -0400227 def _find_caller(self):
228 """Find the caller class and test name.
229
230 Because we know that the interesting things that call us are
231 test_* methods, and various kinds of setUp / tearDown, we
232 can look through the call stack to find appropriate methods,
233 and the class we were in when those were called.
234 """
235 caller_name = None
236 names = []
237 frame = inspect.currentframe()
238 is_cleanup = False
239 # Start climbing the ladder until we hit a good method
240 while True:
241 try:
242 frame = frame.f_back
243 name = frame.f_code.co_name
244 names.append(name)
245 if re.search("^(test_|setUp|tearDown)", name):
246 cname = ""
247 if 'self' in frame.f_locals:
248 cname = frame.f_locals['self'].__class__.__name__
249 if 'cls' in frame.f_locals:
250 cname = frame.f_locals['cls'].__name__
251 caller_name = cname + ":" + name
252 break
253 elif re.search("^_run_cleanup", name):
254 is_cleanup = True
255 else:
256 cname = ""
257 if 'self' in frame.f_locals:
258 cname = frame.f_locals['self'].__class__.__name__
259 if 'cls' in frame.f_locals:
260 cname = frame.f_locals['cls'].__name__
261
262 # the fact that we are running cleanups is indicated pretty
263 # deep in the stack, so if we see that we want to just
264 # start looking for a real class name, and declare victory
265 # once we do.
266 if is_cleanup and cname:
267 if not re.search("^RunTest", cname):
268 caller_name = cname + ":_run_cleanups"
269 break
270 except Exception:
271 break
272 # prevents frame leaks
273 del frame
274 if caller_name is None:
275 self.LOG.debug("Sane call name not found in %s" % names)
276 return caller_name
277
Sean Dague89a85912014-03-19 16:37:29 -0400278 def _get_request_id(self, resp):
279 for i in ('x-openstack-request-id', 'x-compute-request-id'):
280 if i in resp:
281 return resp[i]
282 return ""
Attila Fazekas11d2a772013-01-29 17:46:52 +0100283
Sean Daguec522c092014-03-24 10:43:22 -0400284 def _log_request(self, method, req_url, resp,
285 secs="", req_headers=None,
286 req_body=None, resp_body=None):
Sean Dague0cc47572014-03-20 07:34:05 -0400287 # if we have the request id, put it in the right part of the log
Sean Dague89a85912014-03-19 16:37:29 -0400288 extra = dict(request_id=self._get_request_id(resp))
Sean Dague0cc47572014-03-20 07:34:05 -0400289 # NOTE(sdague): while we still have 6 callers to this function
290 # we're going to just provide work around on who is actually
291 # providing timings by gracefully adding no content if they don't.
292 # Once we're down to 1 caller, clean this up.
Sean Daguec522c092014-03-24 10:43:22 -0400293 caller_name = self._find_caller()
Sean Dague0cc47572014-03-20 07:34:05 -0400294 if secs:
295 secs = " %.3fs" % secs
Sean Dague89a85912014-03-19 16:37:29 -0400296 self.LOG.info(
Sean Dague0cc47572014-03-20 07:34:05 -0400297 'Request (%s): %s %s %s%s' % (
Sean Daguec522c092014-03-24 10:43:22 -0400298 caller_name,
Sean Dague89a85912014-03-19 16:37:29 -0400299 resp['status'],
300 method,
Sean Dague0cc47572014-03-20 07:34:05 -0400301 req_url,
302 secs),
Sean Dague89a85912014-03-19 16:37:29 -0400303 extra=extra)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600304
Sean Daguec522c092014-03-24 10:43:22 -0400305 # We intentionally duplicate the info content because in a parallel
306 # world this is important to match
307 trace_regex = CONF.debug.trace_requests
308 if trace_regex and re.search(trace_regex, caller_name):
309 log_fmt = """Request (%s): %s %s %s%s
310 Request - Headers: %s
311 Body: %s
312 Response - Headers: %s
313 Body: %s"""
314
315 self.LOG.debug(
316 log_fmt % (
317 caller_name,
318 resp['status'],
319 method,
320 req_url,
321 secs,
322 str(req_headers),
323 str(req_body)[:2048],
324 str(resp),
325 str(resp_body)[:2048]),
326 extra=extra)
327
Dan Smithba6cb162012-08-14 07:22:42 -0700328 def _parse_resp(self, body):
vponomaryov67b58fe2014-02-06 19:05:41 +0200329 if self._get_type() is "json":
330 body = json.loads(body)
331
332 # We assume, that if the first value of the deserialized body's
333 # item set is a dict or a list, that we just return the first value
334 # of deserialized body.
335 # Essentially "cutting out" the first placeholder element in a body
336 # that looks like this:
337 #
338 # {
339 # "users": [
340 # ...
341 # ]
342 # }
343 try:
344 # Ensure there are not more than one top-level keys
345 if len(body.keys()) > 1:
346 return body
347 # Just return the "wrapped" element
348 first_key, first_item = body.items()[0]
349 if isinstance(first_item, (dict, list)):
350 return first_item
351 except (ValueError, IndexError):
352 pass
353 return body
354 elif self._get_type() is "xml":
355 element = etree.fromstring(body)
356 if any(s in element.tag for s in self.dict_tags):
357 # Parse dictionary-like xmls (metadata, etc)
358 dictionary = {}
359 for el in element.getchildren():
360 dictionary[u"%s" % el.get("key")] = u"%s" % el.text
361 return dictionary
362 if any(s in element.tag for s in self.list_tags):
363 # Parse list-like xmls (users, roles, etc)
364 array = []
365 for child in element.getchildren():
Masayuki Igawa1edf94f2014-03-04 18:34:16 +0900366 array.append(common.xml_to_json(child))
vponomaryov67b58fe2014-02-06 19:05:41 +0200367 return array
368
369 # Parse one-item-like xmls (user, role, etc)
Masayuki Igawa1edf94f2014-03-04 18:34:16 +0900370 return common.xml_to_json(element)
Dan Smithba6cb162012-08-14 07:22:42 -0700371
Attila Fazekas836e4782013-01-29 15:40:13 +0100372 def response_checker(self, method, url, headers, body, resp, resp_body):
373 if (resp.status in set((204, 205, 304)) or resp.status < 200 or
Pavel Sedláke267eba2013-04-03 15:56:36 +0200374 method.upper() == 'HEAD') and resp_body:
Attila Fazekas836e4782013-01-29 15:40:13 +0100375 raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
Attila Fazekasc3a095b2013-08-17 09:15:44 +0200376 # NOTE(afazekas):
Attila Fazekas836e4782013-01-29 15:40:13 +0100377 # If the HTTP Status Code is 205
378 # 'The response MUST NOT include an entity.'
379 # A HTTP entity has an entity-body and an 'entity-header'.
380 # In the HTTP response specification (Section 6) the 'entity-header'
381 # 'generic-header' and 'response-header' are in OR relation.
382 # All headers not in the above two group are considered as entity
383 # header in every interpretation.
384
385 if (resp.status == 205 and
386 0 != len(set(resp.keys()) - set(('status',)) -
387 self.response_header_lc - self.general_header_lc)):
388 raise exceptions.ResponseWithEntity()
Attila Fazekasc3a095b2013-08-17 09:15:44 +0200389 # NOTE(afazekas)
Attila Fazekas836e4782013-01-29 15:40:13 +0100390 # Now the swift sometimes (delete not empty container)
391 # returns with non json error response, we can create new rest class
392 # for swift.
393 # Usually RFC2616 says error responses SHOULD contain an explanation.
394 # The warning is normal for SHOULD/SHOULD NOT case
395
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100396 # Likely it will cause an error
397 if not resp_body and resp.status >= 400:
Attila Fazekas11d2a772013-01-29 17:46:52 +0100398 self.LOG.warning("status >= 400 response with empty body")
Attila Fazekas836e4782013-01-29 15:40:13 +0100399
vponomaryov67b58fe2014-02-06 19:05:41 +0200400 def _request(self, method, url, headers=None, body=None):
Daryl Wallecke5b83d42011-11-10 14:39:02 -0600401 """A simple HTTP request interface."""
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000402 # Authenticate the request with the auth provider
403 req_url, req_headers, req_body = self.auth_provider.auth_request(
404 method, url, headers, body, self.filters)
Sean Dague89a85912014-03-19 16:37:29 -0400405
Sean Dague0cc47572014-03-20 07:34:05 -0400406 # Do the actual request, and time it
407 start = time.time()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000408 resp, resp_body = self.http_obj.request(
409 req_url, method, headers=req_headers, body=req_body)
Sean Dague0cc47572014-03-20 07:34:05 -0400410 end = time.time()
Sean Daguec522c092014-03-24 10:43:22 -0400411 self._log_request(method, req_url, resp, secs=(end - start),
412 req_headers=req_headers, req_body=req_body,
413 resp_body=resp_body)
414
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000415 # Verify HTTP response codes
416 self.response_checker(method, url, req_headers, req_body, resp,
417 resp_body)
Attila Fazekas72c7a5f2012-12-03 17:17:23 +0100418
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100419 return resp, resp_body
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500420
Sergey Murashov4fccd322014-03-22 09:58:52 +0400421 def request(self, method, url, extra_headers=False, headers=None,
422 body=None):
423 # if extra_headers is True
424 # default headers would be added to headers
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100425 retry = 0
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100426
427 if headers is None:
vponomaryov67b58fe2014-02-06 19:05:41 +0200428 # NOTE(vponomaryov): if some client do not need headers,
429 # it should explicitly pass empty dict
430 headers = self.get_headers()
Sergey Murashov4fccd322014-03-22 09:58:52 +0400431 elif extra_headers:
432 try:
433 headers = headers.copy()
434 headers.update(self.get_headers())
435 except (ValueError, TypeError):
436 headers = self.get_headers()
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100437
438 resp, resp_body = self._request(method, url,
439 headers=headers, body=body)
440
441 while (resp.status == 413 and
442 'retry-after' in resp and
443 not self.is_absolute_limit(
444 resp, self._parse_resp(resp_body)) and
445 retry < MAX_RECURSION_DEPTH):
446 retry += 1
447 delay = int(resp['retry-after'])
448 time.sleep(delay)
449 resp, resp_body = self._request(method, url,
450 headers=headers, body=body)
451 self._error_checker(method, url, headers, body,
452 resp, resp_body)
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500453 return resp, resp_body
454
455 def _error_checker(self, method, url,
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100456 headers, body, resp, resp_body):
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500457
458 # NOTE(mtreinish): Check for httplib response from glance_http. The
459 # object can't be used here because importing httplib breaks httplib2.
460 # If another object from a class not imported were passed here as
461 # resp this could possibly fail
462 if str(type(resp)) == "<type 'instance'>":
463 ctype = resp.getheader('content-type')
464 else:
465 try:
466 ctype = resp['content-type']
467 # NOTE(mtreinish): Keystone delete user responses doesn't have a
468 # content-type header. (They don't have a body) So just pretend it
469 # is set.
470 except KeyError:
471 ctype = 'application/json'
472
Attila Fazekase72b7cd2013-03-26 18:34:21 +0100473 # It is not an error response
474 if resp.status < 400:
475 return
476
Sergey Murashovc10cca52014-01-16 12:48:47 +0400477 JSON_ENC = ['application/json', 'application/json; charset=utf-8']
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500478 # NOTE(mtreinish): This is for compatibility with Glance and swift
479 # APIs. These are the return content types that Glance api v1
480 # (and occasionally swift) are using.
Sergey Murashovc10cca52014-01-16 12:48:47 +0400481 TXT_ENC = ['text/plain', 'text/html', 'text/html; charset=utf-8',
482 'text/plain; charset=utf-8']
483 XML_ENC = ['application/xml', 'application/xml; charset=utf-8']
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500484
Sergey Murashovc10cca52014-01-16 12:48:47 +0400485 if ctype.lower() in JSON_ENC or ctype.lower() in XML_ENC:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500486 parse_resp = True
Sergey Murashovc10cca52014-01-16 12:48:47 +0400487 elif ctype.lower() in TXT_ENC:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500488 parse_resp = False
489 else:
vponomaryov6cb6d192014-03-07 09:39:05 +0200490 raise exceptions.InvalidContentType(str(resp.status))
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500491
Rohit Karajgi6b1e1542012-05-14 05:55:54 -0700492 if resp.status == 401 or resp.status == 403:
Christian Schwede285a8482014-04-09 06:12:55 +0000493 raise exceptions.Unauthorized(resp_body)
Jay Pipes5135bfc2012-01-05 15:46:49 -0500494
495 if resp.status == 404:
Daryl Walleck8a707db2012-01-25 00:46:24 -0600496 raise exceptions.NotFound(resp_body)
Jay Pipes5135bfc2012-01-05 15:46:49 -0500497
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600498 if resp.status == 400:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500499 if parse_resp:
500 resp_body = self._parse_resp(resp_body)
David Kranz28e35c52012-07-10 10:14:38 -0400501 raise exceptions.BadRequest(resp_body)
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600502
David Kranz5a23d862012-02-14 09:48:55 -0500503 if resp.status == 409:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500504 if parse_resp:
505 resp_body = self._parse_resp(resp_body)
Anju5c3e510c2013-10-18 06:40:29 +0530506 raise exceptions.Conflict(resp_body)
David Kranz5a23d862012-02-14 09:48:55 -0500507
Daryl Wallecked8bef32011-12-05 23:02:08 -0600508 if resp.status == 413:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500509 if parse_resp:
510 resp_body = self._parse_resp(resp_body)
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100511 if self.is_absolute_limit(resp, resp_body):
512 raise exceptions.OverLimit(resp_body)
513 else:
514 raise exceptions.RateLimitExceeded(resp_body)
Brian Lamar12d9b292011-12-08 12:41:21 -0500515
Wangpana9b54c62013-02-28 11:04:32 +0800516 if resp.status == 422:
517 if parse_resp:
518 resp_body = self._parse_resp(resp_body)
519 raise exceptions.UnprocessableEntity(resp_body)
520
Daryl Wallecked8bef32011-12-05 23:02:08 -0600521 if resp.status in (500, 501):
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500522 message = resp_body
523 if parse_resp:
Rohan Kanade433994a2013-12-05 22:34:07 +0530524 try:
525 resp_body = self._parse_resp(resp_body)
526 except ValueError:
527 # If response body is a non-json string message.
528 # Use resp_body as is and raise InvalidResponseBody
529 # exception.
530 raise exceptions.InvalidHTTPResponseBody(message)
531 else:
vponomaryov6cb6d192014-03-07 09:39:05 +0200532 if isinstance(resp_body, dict):
533 # I'm seeing both computeFault
534 # and cloudServersFault come back.
535 # Will file a bug to fix, but leave as is for now.
536 if 'cloudServersFault' in resp_body:
537 message = resp_body['cloudServersFault']['message']
538 elif 'computeFault' in resp_body:
539 message = resp_body['computeFault']['message']
540 elif 'error' in resp_body: # Keystone errors
541 message = resp_body['error']['message']
542 raise exceptions.IdentityError(message)
543 elif 'message' in resp_body:
544 message = resp_body['message']
545 else:
546 message = resp_body
Dan Princea4b709c2012-10-10 12:27:59 -0400547
Anju5c3e510c2013-10-18 06:40:29 +0530548 raise exceptions.ServerFault(message)
Daryl Wallecked8bef32011-12-05 23:02:08 -0600549
David Kranz5a23d862012-02-14 09:48:55 -0500550 if resp.status >= 400:
vponomaryov6cb6d192014-03-07 09:39:05 +0200551 raise exceptions.UnexpectedResponseCode(str(resp.status))
David Kranz5a23d862012-02-14 09:48:55 -0500552
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100553 def is_absolute_limit(self, resp, resp_body):
554 if (not isinstance(resp_body, collections.Mapping) or
Pavel Sedláke267eba2013-04-03 15:56:36 +0200555 'retry-after' not in resp):
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100556 return True
vponomaryov67b58fe2014-02-06 19:05:41 +0200557 if self._get_type() is "json":
558 over_limit = resp_body.get('overLimit', None)
559 if not over_limit:
560 return True
561 return 'exceed' in over_limit.get('message', 'blabla')
562 elif self._get_type() is "xml":
563 return 'exceed' in resp_body.get('message', 'blabla')
rajalakshmi-ganesan0275a0d2013-01-11 18:26:05 +0530564
David Kranz6aceb4a2012-06-05 14:05:45 -0400565 def wait_for_resource_deletion(self, id):
Sean Daguef237ccb2013-01-04 15:19:14 -0500566 """Waits for a resource to be deleted."""
David Kranz6aceb4a2012-06-05 14:05:45 -0400567 start_time = int(time.time())
568 while True:
569 if self.is_resource_deleted(id):
570 return
571 if int(time.time()) - start_time >= self.build_timeout:
572 raise exceptions.TimeoutException
573 time.sleep(self.build_interval)
574
575 def is_resource_deleted(self, id):
576 """
577 Subclasses override with specific deletion detection.
578 """
Attila Fazekasd236b4e2013-01-26 00:44:12 +0100579 message = ('"%s" does not implement is_resource_deleted'
580 % self.__class__.__name__)
581 raise NotImplementedError(message)
Dan Smithba6cb162012-08-14 07:22:42 -0700582
Chris Yeohc266b282014-03-13 18:19:00 +1030583 @classmethod
584 def validate_response(cls, schema, resp, body):
585 # Only check the response if the status code is a success code
586 # TODO(cyeoh): Eventually we should be able to verify that a failure
587 # code if it exists is something that we expect. This is explicitly
588 # declared in the V3 API and so we should be able to export this in
589 # the response schema. For now we'll ignore it.
Ken'ichi Ohmichi4e0917c2014-03-19 15:33:47 +0900590 if resp.status in HTTP_SUCCESS:
Chris Yeohc266b282014-03-13 18:19:00 +1030591 response_code = schema['status_code']
592 if resp.status not in response_code:
593 msg = ("The status code(%s) is different than the expected "
594 "one(%s)") % (resp.status, response_code)
595 raise exceptions.InvalidHttpSuccessCode(msg)
596 response_schema = schema.get('response_body')
597 if response_schema:
598 try:
599 jsonschema.validate(body, response_schema)
600 except jsonschema.ValidationError as ex:
601 msg = ("HTTP response body is invalid (%s)") % ex
602 raise exceptions.InvalidHTTPResponseBody(msg)
603 else:
604 if body:
605 msg = ("HTTP response body should not exist (%s)") % body
606 raise exceptions.InvalidHTTPResponseBody(msg)
607
Dan Smithba6cb162012-08-14 07:22:42 -0700608
Marc Koderer24eb89c2014-01-31 11:23:33 +0100609class NegativeRestClient(RestClient):
610 """
611 Version of RestClient that does not raise exceptions.
612 """
613 def _error_checker(self, method, url,
614 headers, body, resp, resp_body):
615 pass
616
617 def send_request(self, method, url_template, resources, body=None):
618 url = url_template % tuple(resources)
619 if method == "GET":
620 resp, body = self.get(url)
621 elif method == "POST":
vponomaryov67b58fe2014-02-06 19:05:41 +0200622 resp, body = self.post(url, body)
Marc Koderer24eb89c2014-01-31 11:23:33 +0100623 elif method == "PUT":
vponomaryov67b58fe2014-02-06 19:05:41 +0200624 resp, body = self.put(url, body)
Marc Koderer24eb89c2014-01-31 11:23:33 +0100625 elif method == "PATCH":
vponomaryov67b58fe2014-02-06 19:05:41 +0200626 resp, body = self.patch(url, body)
Marc Koderer24eb89c2014-01-31 11:23:33 +0100627 elif method == "HEAD":
628 resp, body = self.head(url)
629 elif method == "DELETE":
630 resp, body = self.delete(url)
631 elif method == "COPY":
632 resp, body = self.copy(url)
633 else:
634 assert False
635
636 return resp, body