blob: 81ebd4b1328283574a962cf3860c0da1514642f6 [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
Attila Fazekas11d2a772013-01-29 17:46:52 +010018import hashlib
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
Mate Lakat23a58a32013-08-23 02:06:22 +010024from tempest.common import http
Daryl Wallecked8bef32011-12-05 23:02:08 -060025from tempest import exceptions
Matthew Treinishf4a9b0f2013-07-26 16:58:26 -040026from tempest.openstack.common import log as logging
dwallecke62b9f02012-10-10 23:34:42 -050027from tempest.services.compute.xml.common import xml_to_json
Daryl Walleck1465d612011-11-02 02:22:15 -050028
Eoghan Glynna5598972012-03-01 09:27:17 -050029# redrive rate limited calls at most twice
30MAX_RECURSION_DEPTH = 2
Attila Fazekas11d2a772013-01-29 17:46:52 +010031TOKEN_CHARS_RE = re.compile('^[-A-Za-z0-9+/=]*$')
Eoghan Glynna5598972012-03-01 09:27:17 -050032
Attila Fazekas54a42862013-07-28 22:31:06 +020033# All the successful HTTP status codes from RFC 2616
34HTTP_SUCCESS = (200, 201, 202, 203, 204, 205, 206)
35
Eoghan Glynna5598972012-03-01 09:27:17 -050036
Daryl Walleck1465d612011-11-02 02:22:15 -050037class RestClient(object):
Dan Smithba6cb162012-08-14 07:22:42 -070038 TYPE = "json"
Attila Fazekas11d2a772013-01-29 17:46:52 +010039 LOG = logging.getLogger(__name__)
Daryl Walleck1465d612011-11-02 02:22:15 -050040
Brant Knudsonc7ca3342013-03-28 21:08:50 -050041 def __init__(self, config, user, password, auth_url, tenant_name=None,
42 auth_version='v2'):
Jay Pipes7f757632011-12-02 15:53:32 -050043 self.config = config
chris fattarsi5098fa22012-04-17 13:27:00 -070044 self.user = user
45 self.password = password
46 self.auth_url = auth_url
47 self.tenant_name = tenant_name
Brant Knudsonc7ca3342013-03-28 21:08:50 -050048 self.auth_version = auth_version
chris fattarsi5098fa22012-04-17 13:27:00 -070049
50 self.service = None
51 self.token = None
52 self.base_url = None
Arata Notsu8f440392013-09-13 16:14:20 +090053 self.region = {}
54 for cfgname in dir(self.config):
55 # Find all config.FOO.catalog_type and assume FOO is a service.
56 cfg = getattr(self.config, cfgname)
57 catalog_type = getattr(cfg, 'catalog_type', None)
58 if not catalog_type:
59 continue
60 service_region = getattr(cfg, 'region', None)
61 if not service_region:
62 service_region = self.config.identity.region
63 self.region[catalog_type] = service_region
chris fattarsi5098fa22012-04-17 13:27:00 -070064 self.endpoint_url = 'publicURL'
Dan Smithba6cb162012-08-14 07:22:42 -070065 self.headers = {'Content-Type': 'application/%s' % self.TYPE,
66 'Accept': 'application/%s' % self.TYPE}
David Kranz6aceb4a2012-06-05 14:05:45 -040067 self.build_interval = config.compute.build_interval
68 self.build_timeout = config.compute.build_timeout
Attila Fazekas72c7a5f2012-12-03 17:17:23 +010069 self.general_header_lc = set(('cache-control', 'connection',
70 'date', 'pragma', 'trailer',
71 'transfer-encoding', 'via',
72 'warning'))
73 self.response_header_lc = set(('accept-ranges', 'age', 'etag',
74 'location', 'proxy-authenticate',
75 'retry-after', 'server',
76 'vary', 'www-authenticate'))
Attila Fazekas55f6d8c2013-03-10 10:32:54 +010077 dscv = self.config.identity.disable_ssl_certificate_validation
Mate Lakat23a58a32013-08-23 02:06:22 +010078 self.http_obj = http.ClosingHttp(
79 disable_ssl_certificate_validation=dscv)
chris fattarsi5098fa22012-04-17 13:27:00 -070080
DennyZhang7be75002013-09-19 06:55:11 -050081 def __str__(self):
82 STRING_LIMIT = 80
83 str_format = ("config:%s, user:%s, password:%s, "
84 "auth_url:%s, tenant_name:%s, auth_version:%s, "
85 "service:%s, base_url:%s, region:%s, "
86 "endpoint_url:%s, build_interval:%s, build_timeout:%s"
87 "\ntoken:%s..., \nheaders:%s...")
88 return str_format % (self.config, self.user, self.password,
89 self.auth_url, self.tenant_name,
90 self.auth_version, self.service,
91 self.base_url, self.region, self.endpoint_url,
92 self.build_interval, self.build_timeout,
93 str(self.token)[0:STRING_LIMIT],
94 str(self.headers)[0:STRING_LIMIT])
95
chris fattarsi5098fa22012-04-17 13:27:00 -070096 def _set_auth(self):
97 """
98 Sets the token and base_url used in requests based on the strategy type
99 """
100
Li Ma216550f2013-06-12 11:26:08 -0700101 if self.auth_version == 'v3':
102 auth_func = self.identity_auth_v3
Daryl Walleck1465d612011-11-02 02:22:15 -0500103 else:
Li Ma216550f2013-06-12 11:26:08 -0700104 auth_func = self.keystone_auth
105
106 self.token, self.base_url = (
107 auth_func(self.user, self.password, self.auth_url,
108 self.service, self.tenant_name))
chris fattarsi5098fa22012-04-17 13:27:00 -0700109
110 def clear_auth(self):
111 """
112 Can be called to clear the token and base_url so that the next request
Attila Fazekasb2902af2013-02-16 16:22:44 +0100113 will fetch a new token and base_url.
chris fattarsi5098fa22012-04-17 13:27:00 -0700114 """
115
116 self.token = None
117 self.base_url = None
Daryl Walleck1465d612011-11-02 02:22:15 -0500118
Rohit Karajgi6b1e1542012-05-14 05:55:54 -0700119 def get_auth(self):
120 """Returns the token of the current request or sets the token if
Attila Fazekasb2902af2013-02-16 16:22:44 +0100121 none.
122 """
Rohit Karajgi6b1e1542012-05-14 05:55:54 -0700123
124 if not self.token:
125 self._set_auth()
126
127 return self.token
128
Daryl Walleck587385b2012-03-03 13:00:26 -0600129 def basic_auth(self, user, password, auth_url):
Daryl Walleck1465d612011-11-02 02:22:15 -0500130 """
Attila Fazekasb2902af2013-02-16 16:22:44 +0100131 Provides authentication for the target API.
Daryl Walleck1465d612011-11-02 02:22:15 -0500132 """
133
134 params = {}
135 params['headers'] = {'User-Agent': 'Test-Client', 'X-Auth-User': user,
Daryl Walleck587385b2012-03-03 13:00:26 -0600136 'X-Auth-Key': password}
Daryl Walleck1465d612011-11-02 02:22:15 -0500137
Daryl Walleck1465d612011-11-02 02:22:15 -0500138 resp, body = self.http_obj.request(auth_url, 'GET', **params)
139 try:
140 return resp['x-auth-token'], resp['x-server-management-url']
Matthew Treinish05d9fb92012-12-07 16:14:05 -0500141 except Exception:
Daryl Walleck1465d612011-11-02 02:22:15 -0500142 raise
143
Daryl Walleck587385b2012-03-03 13:00:26 -0600144 def keystone_auth(self, user, password, auth_url, service, tenant_name):
Daryl Walleck1465d612011-11-02 02:22:15 -0500145 """
Brant Knudsonc7ca3342013-03-28 21:08:50 -0500146 Provides authentication via Keystone using v2 identity API.
Daryl Walleck1465d612011-11-02 02:22:15 -0500147 """
148
Jay Pipes7c88eb22013-01-16 21:32:43 -0500149 # Normalize URI to ensure /tokens is in it.
150 if 'tokens' not in auth_url:
151 auth_url = auth_url.rstrip('/') + '/tokens'
152
Zhongyue Luo30a563f2012-09-30 23:43:50 +0900153 creds = {
154 'auth': {
Daryl Walleck1465d612011-11-02 02:22:15 -0500155 'passwordCredentials': {
156 'username': user,
Daryl Walleck587385b2012-03-03 13:00:26 -0600157 'password': password,
Daryl Walleck1465d612011-11-02 02:22:15 -0500158 },
Zhongyue Luo30a563f2012-09-30 23:43:50 +0900159 'tenantName': tenant_name,
Daryl Walleck1465d612011-11-02 02:22:15 -0500160 }
161 }
162
Daryl Walleck1465d612011-11-02 02:22:15 -0500163 headers = {'Content-Type': 'application/json'}
164 body = json.dumps(creds)
Pavel Sedláke267eba2013-04-03 15:56:36 +0200165 self._log_request('POST', auth_url, headers, body)
166 resp, resp_body = self.http_obj.request(auth_url, 'POST',
167 headers=headers, body=body)
168 self._log_response(resp, resp_body)
Daryl Walleck1465d612011-11-02 02:22:15 -0500169
Jay Pipes7f757632011-12-02 15:53:32 -0500170 if resp.status == 200:
171 try:
Pavel Sedláke267eba2013-04-03 15:56:36 +0200172 auth_data = json.loads(resp_body)['access']
Jay Pipes7f757632011-12-02 15:53:32 -0500173 token = auth_data['token']['id']
Dirk Mueller1db5db22013-06-23 20:21:32 +0200174 except Exception as e:
175 print("Failed to obtain token for user: %s" % e)
Jay Pipes7f757632011-12-02 15:53:32 -0500176 raise
Adam Gandelmane2d46b42012-01-03 17:40:44 -0800177
178 mgmt_url = None
179 for ep in auth_data['serviceCatalog']:
Dan Prince8527c8a2012-12-14 14:00:31 -0500180 if ep["type"] == service:
K Jonathan Harkerd6ba4b42012-12-18 13:50:47 -0800181 for _ep in ep['endpoints']:
182 if service in self.region and \
183 _ep['region'] == self.region[service]:
184 mgmt_url = _ep[self.endpoint_url]
185 if not mgmt_url:
186 mgmt_url = ep['endpoints'][0][self.endpoint_url]
Adam Gandelmane2d46b42012-01-03 17:40:44 -0800187 break
188
Zhongyue Luoe471d6e2012-09-17 17:02:43 +0800189 if mgmt_url is None:
Adam Gandelmane2d46b42012-01-03 17:40:44 -0800190 raise exceptions.EndpointNotFound(service)
191
Rohit Karajgid2a28af2012-05-23 03:44:59 -0700192 return token, mgmt_url
193
Jay Pipes7f757632011-12-02 15:53:32 -0500194 elif resp.status == 401:
Daryl Wallecka22f57b2012-03-20 16:52:07 -0500195 raise exceptions.AuthenticationFailure(user=user,
Matthew Treinishb86cda92013-07-29 11:22:23 -0400196 password=password,
197 tenant=tenant_name)
Pavel Sedláke267eba2013-04-03 15:56:36 +0200198 raise exceptions.IdentityError('Unexpected status code {0}'.format(
199 resp.status))
Daryl Walleck1465d612011-11-02 02:22:15 -0500200
Brant Knudsonc7ca3342013-03-28 21:08:50 -0500201 def identity_auth_v3(self, user, password, auth_url, service,
202 project_name, domain_id='default'):
203 """Provides authentication using Identity API v3."""
204
205 req_url = auth_url.rstrip('/') + '/auth/tokens'
206
207 creds = {
208 "auth": {
209 "identity": {
210 "methods": ["password"],
211 "password": {
212 "user": {
213 "name": user, "password": password,
214 "domain": {"id": domain_id}
215 }
216 }
217 },
218 "scope": {
219 "project": {
220 "domain": {"id": domain_id},
221 "name": project_name
222 }
223 }
224 }
225 }
226
227 headers = {'Content-Type': 'application/json'}
228 body = json.dumps(creds)
229 resp, body = self.http_obj.request(req_url, 'POST',
230 headers=headers, body=body)
231
232 if resp.status == 201:
233 try:
234 token = resp['x-subject-token']
235 except Exception:
236 self.LOG.exception("Failed to obtain token using V3"
237 " authentication (auth URL is '%s')" %
238 req_url)
239 raise
240
241 catalog = json.loads(body)['token']['catalog']
242
243 mgmt_url = None
244 for service_info in catalog:
245 if service_info['type'] != service:
246 continue # this isn't the entry for us.
247
248 endpoints = service_info['endpoints']
249
250 # Look for an endpoint in the region if configured.
251 if service in self.region:
252 region = self.region[service]
253
254 for ep in endpoints:
255 if ep['region'] != region:
256 continue
257
258 mgmt_url = ep['url']
259 # FIXME(blk-u): this isn't handling endpoint type
260 # (public, internal, admin).
261 break
262
263 if not mgmt_url:
264 # Didn't find endpoint for region, use the first.
265
266 ep = endpoints[0]
267 mgmt_url = ep['url']
268 # FIXME(blk-u): this isn't handling endpoint type
269 # (public, internal, admin).
270
271 break
272
273 return token, mgmt_url
274
275 elif resp.status == 401:
276 raise exceptions.AuthenticationFailure(user=user,
277 password=password)
278 else:
279 self.LOG.error("Failed to obtain token using V3 authentication"
280 " (auth URL is '%s'), the response status is %s" %
281 (req_url, resp.status))
282 raise exceptions.AuthenticationFailure(user=user,
283 password=password)
284
Attila Fazekas54a42862013-07-28 22:31:06 +0200285 def expected_success(self, expected_code, read_code):
286 assert_msg = ("This function only allowed to use for HTTP status"
287 "codes which explicitly defined in the RFC 2616. {0}"
288 " is not a defined Success Code!").format(expected_code)
289 assert expected_code in HTTP_SUCCESS, assert_msg
290
291 # NOTE(afazekas): the http status code above 400 is processed by
292 # the _error_checker method
293 if read_code < 400 and read_code != expected_code:
294 pattern = """Unexpected http success status code {0},
295 The expected status code is {1}"""
296 details = pattern.format(read_code, expected_code)
297 raise exceptions.InvalidHttpSuccessCode(details)
298
Daryl Walleck1465d612011-11-02 02:22:15 -0500299 def post(self, url, body, headers):
300 return self.request('POST', url, headers, body)
301
Attila Fazekasb8aa7592013-01-26 01:25:45 +0100302 def get(self, url, headers=None):
303 return self.request('GET', url, headers)
Daryl Walleck1465d612011-11-02 02:22:15 -0500304
Dan Smithba6cb162012-08-14 07:22:42 -0700305 def delete(self, url, headers=None):
306 return self.request('DELETE', url, headers)
Daryl Walleck1465d612011-11-02 02:22:15 -0500307
rajalakshmi-ganesanab426722013-02-08 15:49:15 +0530308 def patch(self, url, body, headers):
309 return self.request('PATCH', url, headers, body)
310
Daryl Walleck1465d612011-11-02 02:22:15 -0500311 def put(self, url, body, headers):
312 return self.request('PUT', url, headers, body)
313
dwalleck5d734432012-10-04 01:11:47 -0500314 def head(self, url, headers=None):
Larisa Ustalov6c3c7802012-11-05 12:25:19 +0200315 return self.request('HEAD', url, headers)
316
317 def copy(self, url, headers=None):
318 return self.request('COPY', url, headers)
dwalleck5d734432012-10-04 01:11:47 -0500319
Matthew Treinishc0f768f2013-03-11 14:24:16 -0400320 def get_versions(self):
321 resp, body = self.get('')
322 body = self._parse_resp(body)
323 body = body['versions']
324 versions = map(lambda x: x['id'], body)
325 return resp, versions
326
Attila Fazekas11d2a772013-01-29 17:46:52 +0100327 def _log_request(self, method, req_url, headers, body):
328 self.LOG.info('Request: ' + method + ' ' + req_url)
329 if headers:
330 print_headers = headers
331 if 'X-Auth-Token' in headers and headers['X-Auth-Token']:
332 token = headers['X-Auth-Token']
333 if len(token) > 64 and TOKEN_CHARS_RE.match(token):
334 print_headers = headers.copy()
335 print_headers['X-Auth-Token'] = "<Token omitted>"
336 self.LOG.debug('Request Headers: ' + str(print_headers))
337 if body:
338 str_body = str(body)
339 length = len(str_body)
340 self.LOG.debug('Request Body: ' + str_body[:2048])
341 if length >= 2048:
342 self.LOG.debug("Large body (%d) md5 summary: %s", length,
343 hashlib.md5(str_body).hexdigest())
344
345 def _log_response(self, resp, resp_body):
346 status = resp['status']
347 self.LOG.info("Response Status: " + status)
348 headers = resp.copy()
349 del headers['status']
Matthew Treinishe5423912013-08-13 18:07:31 -0400350 if headers.get('x-compute-request-id'):
351 self.LOG.info("Nova request id: %s" %
352 headers.pop('x-compute-request-id'))
353 elif headers.get('x-openstack-request-id'):
354 self.LOG.info("Glance request id %s" %
355 headers.pop('x-openstack-request-id'))
Attila Fazekas11d2a772013-01-29 17:46:52 +0100356 if len(headers):
357 self.LOG.debug('Response Headers: ' + str(headers))
358 if resp_body:
359 str_body = str(resp_body)
360 length = len(str_body)
361 self.LOG.debug('Response Body: ' + str_body[:2048])
362 if length >= 2048:
363 self.LOG.debug("Large body (%d) md5 summary: %s", length,
364 hashlib.md5(str_body).hexdigest())
Daryl Walleck8a707db2012-01-25 00:46:24 -0600365
Dan Smithba6cb162012-08-14 07:22:42 -0700366 def _parse_resp(self, body):
367 return json.loads(body)
368
Attila Fazekas836e4782013-01-29 15:40:13 +0100369 def response_checker(self, method, url, headers, body, resp, resp_body):
370 if (resp.status in set((204, 205, 304)) or resp.status < 200 or
Pavel Sedláke267eba2013-04-03 15:56:36 +0200371 method.upper() == 'HEAD') and resp_body:
Attila Fazekas836e4782013-01-29 15:40:13 +0100372 raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
Attila Fazekasc3a095b2013-08-17 09:15:44 +0200373 # NOTE(afazekas):
Attila Fazekas836e4782013-01-29 15:40:13 +0100374 # If the HTTP Status Code is 205
375 # 'The response MUST NOT include an entity.'
376 # A HTTP entity has an entity-body and an 'entity-header'.
377 # In the HTTP response specification (Section 6) the 'entity-header'
378 # 'generic-header' and 'response-header' are in OR relation.
379 # All headers not in the above two group are considered as entity
380 # header in every interpretation.
381
382 if (resp.status == 205 and
383 0 != len(set(resp.keys()) - set(('status',)) -
384 self.response_header_lc - self.general_header_lc)):
385 raise exceptions.ResponseWithEntity()
Attila Fazekasc3a095b2013-08-17 09:15:44 +0200386 # NOTE(afazekas)
Attila Fazekas836e4782013-01-29 15:40:13 +0100387 # Now the swift sometimes (delete not empty container)
388 # returns with non json error response, we can create new rest class
389 # for swift.
390 # Usually RFC2616 says error responses SHOULD contain an explanation.
391 # The warning is normal for SHOULD/SHOULD NOT case
392
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100393 # Likely it will cause an error
394 if not resp_body and resp.status >= 400:
Attila Fazekas11d2a772013-01-29 17:46:52 +0100395 self.LOG.warning("status >= 400 response with empty body")
Attila Fazekas836e4782013-01-29 15:40:13 +0100396
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100397 def _request(self, method, url,
398 headers=None, body=None):
Daryl Wallecke5b83d42011-11-10 14:39:02 -0600399 """A simple HTTP request interface."""
Daryl Walleck1465d612011-11-02 02:22:15 -0500400
Daryl Walleck1465d612011-11-02 02:22:15 -0500401 req_url = "%s/%s" % (self.base_url, url)
Attila Fazekas11d2a772013-01-29 17:46:52 +0100402 self._log_request(method, req_url, headers, body)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600403 resp, resp_body = self.http_obj.request(req_url, method,
Zhongyue Luo79d8d362012-09-25 13:49:27 +0800404 headers=headers, body=body)
Attila Fazekas11d2a772013-01-29 17:46:52 +0100405 self._log_response(resp, resp_body)
Attila Fazekas836e4782013-01-29 15:40:13 +0100406 self.response_checker(method, url, headers, body, resp, resp_body)
Attila Fazekas72c7a5f2012-12-03 17:17:23 +0100407
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100408 return resp, resp_body
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500409
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100410 def request(self, method, url,
411 headers=None, body=None):
412 retry = 0
413 if (self.token is None) or (self.base_url is None):
414 self._set_auth()
415
416 if headers is None:
417 headers = {}
418 headers['X-Auth-Token'] = self.token
419
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']
465 XML_ENC = ['application/xml', 'application/xml; charset=utf-8']
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500466
Sergey Murashovc10cca52014-01-16 12:48:47 +0400467 if ctype.lower() in JSON_ENC or ctype.lower() in XML_ENC:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500468 parse_resp = True
Sergey Murashovc10cca52014-01-16 12:48:47 +0400469 elif ctype.lower() in TXT_ENC:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500470 parse_resp = False
471 else:
472 raise exceptions.RestClientException(str(resp.status))
473
Rohit Karajgi6b1e1542012-05-14 05:55:54 -0700474 if resp.status == 401 or resp.status == 403:
Daryl Walleckced8eb82012-03-19 13:52:37 -0500475 raise exceptions.Unauthorized()
Jay Pipes5135bfc2012-01-05 15:46:49 -0500476
477 if resp.status == 404:
Daryl Walleck8a707db2012-01-25 00:46:24 -0600478 raise exceptions.NotFound(resp_body)
Jay Pipes5135bfc2012-01-05 15:46:49 -0500479
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600480 if resp.status == 400:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500481 if parse_resp:
482 resp_body = self._parse_resp(resp_body)
David Kranz28e35c52012-07-10 10:14:38 -0400483 raise exceptions.BadRequest(resp_body)
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600484
David Kranz5a23d862012-02-14 09:48:55 -0500485 if resp.status == 409:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500486 if parse_resp:
487 resp_body = self._parse_resp(resp_body)
Anju5c3e510c2013-10-18 06:40:29 +0530488 raise exceptions.Conflict(resp_body)
David Kranz5a23d862012-02-14 09:48:55 -0500489
Daryl Wallecked8bef32011-12-05 23:02:08 -0600490 if resp.status == 413:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500491 if parse_resp:
492 resp_body = self._parse_resp(resp_body)
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100493 if self.is_absolute_limit(resp, resp_body):
494 raise exceptions.OverLimit(resp_body)
495 else:
496 raise exceptions.RateLimitExceeded(resp_body)
Brian Lamar12d9b292011-12-08 12:41:21 -0500497
Wangpana9b54c62013-02-28 11:04:32 +0800498 if resp.status == 422:
499 if parse_resp:
500 resp_body = self._parse_resp(resp_body)
501 raise exceptions.UnprocessableEntity(resp_body)
502
Daryl Wallecked8bef32011-12-05 23:02:08 -0600503 if resp.status in (500, 501):
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500504 message = resp_body
505 if parse_resp:
Rohan Kanade433994a2013-12-05 22:34:07 +0530506 try:
507 resp_body = self._parse_resp(resp_body)
508 except ValueError:
509 # If response body is a non-json string message.
510 # Use resp_body as is and raise InvalidResponseBody
511 # exception.
512 raise exceptions.InvalidHTTPResponseBody(message)
513 else:
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']
521 elif 'error' in resp_body: # Keystone errors
522 message = resp_body['error']['message']
523 raise exceptions.IdentityError(message)
524 elif 'message' in resp_body:
525 message = resp_body['message']
Dan Princea4b709c2012-10-10 12:27:59 -0400526
Anju5c3e510c2013-10-18 06:40:29 +0530527 raise exceptions.ServerFault(message)
Daryl Wallecked8bef32011-12-05 23:02:08 -0600528
David Kranz5a23d862012-02-14 09:48:55 -0500529 if resp.status >= 400:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500530 if parse_resp:
531 resp_body = self._parse_resp(resp_body)
Attila Fazekas96524032013-01-29 19:52:49 +0100532 raise exceptions.RestClientException(str(resp.status))
David Kranz5a23d862012-02-14 09:48:55 -0500533
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100534 def is_absolute_limit(self, resp, resp_body):
535 if (not isinstance(resp_body, collections.Mapping) or
Pavel Sedláke267eba2013-04-03 15:56:36 +0200536 'retry-after' not in resp):
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100537 return True
538 over_limit = resp_body.get('overLimit', None)
539 if not over_limit:
540 return True
541 return 'exceed' in over_limit.get('message', 'blabla')
rajalakshmi-ganesan0275a0d2013-01-11 18:26:05 +0530542
David Kranz6aceb4a2012-06-05 14:05:45 -0400543 def wait_for_resource_deletion(self, id):
Sean Daguef237ccb2013-01-04 15:19:14 -0500544 """Waits for a resource to be deleted."""
David Kranz6aceb4a2012-06-05 14:05:45 -0400545 start_time = int(time.time())
546 while True:
547 if self.is_resource_deleted(id):
548 return
549 if int(time.time()) - start_time >= self.build_timeout:
550 raise exceptions.TimeoutException
551 time.sleep(self.build_interval)
552
553 def is_resource_deleted(self, id):
554 """
555 Subclasses override with specific deletion detection.
556 """
Attila Fazekasd236b4e2013-01-26 00:44:12 +0100557 message = ('"%s" does not implement is_resource_deleted'
558 % self.__class__.__name__)
559 raise NotImplementedError(message)
Dan Smithba6cb162012-08-14 07:22:42 -0700560
561
562class RestClientXML(RestClient):
563 TYPE = "xml"
564
565 def _parse_resp(self, body):
566 return xml_to_json(etree.fromstring(body))
rajalakshmi-ganesan0275a0d2013-01-11 18:26:05 +0530567
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100568 def is_absolute_limit(self, resp, resp_body):
569 if (not isinstance(resp_body, collections.Mapping) or
Pavel Sedláke267eba2013-04-03 15:56:36 +0200570 'retry-after' not in resp):
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100571 return True
572 return 'exceed' in resp_body.get('message', 'blabla')