blob: 4c4d61b4d43dc9983e9414a8dd00280deaf19194 [file] [log] [blame]
Jay Pipes3f981df2012-03-27 18:59:44 -04001# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2012 OpenStack, LLC
Brant Knudsonc7ca3342013-03-28 21:08:50 -05004# Copyright 2013 IBM Corp.
Jay Pipes3f981df2012-03-27 18:59:44 -04005# All Rights Reserved.
6#
7# Licensed under the Apache License, Version 2.0 (the "License"); you may
8# not use this file except in compliance with the License. You may obtain
9# a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16# License for the specific language governing permissions and limitations
17# under the License.
18
Attila Fazekas55f6d8c2013-03-10 10:32:54 +010019import collections
Attila Fazekas11d2a772013-01-29 17:46:52 +010020import hashlib
Jay Pipes7f757632011-12-02 15:53:32 -050021import httplib2
Matthew Treinisha83a16e2012-12-07 13:44:02 -050022import json
Dan Smithba6cb162012-08-14 07:22:42 -070023from lxml import etree
Attila Fazekas11d2a772013-01-29 17:46:52 +010024import re
Eoghan Glynna5598972012-03-01 09:27:17 -050025import time
Jay Pipes3f981df2012-03-27 18:59:44 -040026
Daryl Wallecked8bef32011-12-05 23:02:08 -060027from tempest import exceptions
Matthew Treinishf4a9b0f2013-07-26 16:58:26 -040028from tempest.openstack.common import log as logging
dwallecke62b9f02012-10-10 23:34:42 -050029from tempest.services.compute.xml.common import xml_to_json
Daryl Walleck1465d612011-11-02 02:22:15 -050030
Eoghan Glynna5598972012-03-01 09:27:17 -050031# redrive rate limited calls at most twice
32MAX_RECURSION_DEPTH = 2
Attila Fazekas11d2a772013-01-29 17:46:52 +010033TOKEN_CHARS_RE = re.compile('^[-A-Za-z0-9+/=]*$')
Eoghan Glynna5598972012-03-01 09:27:17 -050034
35
Daryl Walleck1465d612011-11-02 02:22:15 -050036class RestClient(object):
Dan Smithba6cb162012-08-14 07:22:42 -070037 TYPE = "json"
Attila Fazekas11d2a772013-01-29 17:46:52 +010038 LOG = logging.getLogger(__name__)
Daryl Walleck1465d612011-11-02 02:22:15 -050039
Brant Knudsonc7ca3342013-03-28 21:08:50 -050040 def __init__(self, config, user, password, auth_url, tenant_name=None,
41 auth_version='v2'):
Jay Pipes7f757632011-12-02 15:53:32 -050042 self.config = config
chris fattarsi5098fa22012-04-17 13:27:00 -070043 self.user = user
44 self.password = password
45 self.auth_url = auth_url
46 self.tenant_name = tenant_name
Brant Knudsonc7ca3342013-03-28 21:08:50 -050047 self.auth_version = auth_version
chris fattarsi5098fa22012-04-17 13:27:00 -070048
49 self.service = None
50 self.token = None
51 self.base_url = None
Attila Fazekascadcb1f2013-01-21 23:10:53 +010052 self.region = {'compute': self.config.identity.region}
chris fattarsi5098fa22012-04-17 13:27:00 -070053 self.endpoint_url = 'publicURL'
Dan Smithba6cb162012-08-14 07:22:42 -070054 self.headers = {'Content-Type': 'application/%s' % self.TYPE,
55 'Accept': 'application/%s' % self.TYPE}
David Kranz6aceb4a2012-06-05 14:05:45 -040056 self.build_interval = config.compute.build_interval
57 self.build_timeout = config.compute.build_timeout
Attila Fazekas72c7a5f2012-12-03 17:17:23 +010058 self.general_header_lc = set(('cache-control', 'connection',
59 'date', 'pragma', 'trailer',
60 'transfer-encoding', 'via',
61 'warning'))
62 self.response_header_lc = set(('accept-ranges', 'age', 'etag',
63 'location', 'proxy-authenticate',
64 'retry-after', 'server',
65 'vary', 'www-authenticate'))
Attila Fazekas55f6d8c2013-03-10 10:32:54 +010066 dscv = self.config.identity.disable_ssl_certificate_validation
67 self.http_obj = httplib2.Http(disable_ssl_certificate_validation=dscv)
chris fattarsi5098fa22012-04-17 13:27:00 -070068
69 def _set_auth(self):
70 """
71 Sets the token and base_url used in requests based on the strategy type
72 """
73
Li Ma216550f2013-06-12 11:26:08 -070074 if self.auth_version == 'v3':
75 auth_func = self.identity_auth_v3
Daryl Walleck1465d612011-11-02 02:22:15 -050076 else:
Li Ma216550f2013-06-12 11:26:08 -070077 auth_func = self.keystone_auth
78
79 self.token, self.base_url = (
80 auth_func(self.user, self.password, self.auth_url,
81 self.service, self.tenant_name))
chris fattarsi5098fa22012-04-17 13:27:00 -070082
83 def clear_auth(self):
84 """
85 Can be called to clear the token and base_url so that the next request
Attila Fazekasb2902af2013-02-16 16:22:44 +010086 will fetch a new token and base_url.
chris fattarsi5098fa22012-04-17 13:27:00 -070087 """
88
89 self.token = None
90 self.base_url = None
Daryl Walleck1465d612011-11-02 02:22:15 -050091
Rohit Karajgi6b1e1542012-05-14 05:55:54 -070092 def get_auth(self):
93 """Returns the token of the current request or sets the token if
Attila Fazekasb2902af2013-02-16 16:22:44 +010094 none.
95 """
Rohit Karajgi6b1e1542012-05-14 05:55:54 -070096
97 if not self.token:
98 self._set_auth()
99
100 return self.token
101
Daryl Walleck587385b2012-03-03 13:00:26 -0600102 def basic_auth(self, user, password, auth_url):
Daryl Walleck1465d612011-11-02 02:22:15 -0500103 """
Attila Fazekasb2902af2013-02-16 16:22:44 +0100104 Provides authentication for the target API.
Daryl Walleck1465d612011-11-02 02:22:15 -0500105 """
106
107 params = {}
108 params['headers'] = {'User-Agent': 'Test-Client', 'X-Auth-User': user,
Daryl Walleck587385b2012-03-03 13:00:26 -0600109 'X-Auth-Key': password}
Daryl Walleck1465d612011-11-02 02:22:15 -0500110
Daryl Walleck1465d612011-11-02 02:22:15 -0500111 resp, body = self.http_obj.request(auth_url, 'GET', **params)
112 try:
113 return resp['x-auth-token'], resp['x-server-management-url']
Matthew Treinish05d9fb92012-12-07 16:14:05 -0500114 except Exception:
Daryl Walleck1465d612011-11-02 02:22:15 -0500115 raise
116
Daryl Walleck587385b2012-03-03 13:00:26 -0600117 def keystone_auth(self, user, password, auth_url, service, tenant_name):
Daryl Walleck1465d612011-11-02 02:22:15 -0500118 """
Brant Knudsonc7ca3342013-03-28 21:08:50 -0500119 Provides authentication via Keystone using v2 identity API.
Daryl Walleck1465d612011-11-02 02:22:15 -0500120 """
121
Jay Pipes7c88eb22013-01-16 21:32:43 -0500122 # Normalize URI to ensure /tokens is in it.
123 if 'tokens' not in auth_url:
124 auth_url = auth_url.rstrip('/') + '/tokens'
125
Zhongyue Luo30a563f2012-09-30 23:43:50 +0900126 creds = {
127 'auth': {
Daryl Walleck1465d612011-11-02 02:22:15 -0500128 'passwordCredentials': {
129 'username': user,
Daryl Walleck587385b2012-03-03 13:00:26 -0600130 'password': password,
Daryl Walleck1465d612011-11-02 02:22:15 -0500131 },
Zhongyue Luo30a563f2012-09-30 23:43:50 +0900132 'tenantName': tenant_name,
Daryl Walleck1465d612011-11-02 02:22:15 -0500133 }
134 }
135
Daryl Walleck1465d612011-11-02 02:22:15 -0500136 headers = {'Content-Type': 'application/json'}
137 body = json.dumps(creds)
Pavel Sedláke267eba2013-04-03 15:56:36 +0200138 self._log_request('POST', auth_url, headers, body)
139 resp, resp_body = self.http_obj.request(auth_url, 'POST',
140 headers=headers, body=body)
141 self._log_response(resp, resp_body)
Daryl Walleck1465d612011-11-02 02:22:15 -0500142
Jay Pipes7f757632011-12-02 15:53:32 -0500143 if resp.status == 200:
144 try:
Pavel Sedláke267eba2013-04-03 15:56:36 +0200145 auth_data = json.loads(resp_body)['access']
Jay Pipes7f757632011-12-02 15:53:32 -0500146 token = auth_data['token']['id']
Dirk Mueller1db5db22013-06-23 20:21:32 +0200147 except Exception as e:
148 print("Failed to obtain token for user: %s" % e)
Jay Pipes7f757632011-12-02 15:53:32 -0500149 raise
Adam Gandelmane2d46b42012-01-03 17:40:44 -0800150
151 mgmt_url = None
152 for ep in auth_data['serviceCatalog']:
Dan Prince8527c8a2012-12-14 14:00:31 -0500153 if ep["type"] == service:
K Jonathan Harkerd6ba4b42012-12-18 13:50:47 -0800154 for _ep in ep['endpoints']:
155 if service in self.region and \
156 _ep['region'] == self.region[service]:
157 mgmt_url = _ep[self.endpoint_url]
158 if not mgmt_url:
159 mgmt_url = ep['endpoints'][0][self.endpoint_url]
Adam Gandelmane2d46b42012-01-03 17:40:44 -0800160 break
161
Zhongyue Luoe471d6e2012-09-17 17:02:43 +0800162 if mgmt_url is None:
Adam Gandelmane2d46b42012-01-03 17:40:44 -0800163 raise exceptions.EndpointNotFound(service)
164
Rohit Karajgid2a28af2012-05-23 03:44:59 -0700165 return token, mgmt_url
166
Jay Pipes7f757632011-12-02 15:53:32 -0500167 elif resp.status == 401:
Daryl Wallecka22f57b2012-03-20 16:52:07 -0500168 raise exceptions.AuthenticationFailure(user=user,
Matthew Treinishb86cda92013-07-29 11:22:23 -0400169 password=password,
170 tenant=tenant_name)
Pavel Sedláke267eba2013-04-03 15:56:36 +0200171 raise exceptions.IdentityError('Unexpected status code {0}'.format(
172 resp.status))
Daryl Walleck1465d612011-11-02 02:22:15 -0500173
Brant Knudsonc7ca3342013-03-28 21:08:50 -0500174 def identity_auth_v3(self, user, password, auth_url, service,
175 project_name, domain_id='default'):
176 """Provides authentication using Identity API v3."""
177
178 req_url = auth_url.rstrip('/') + '/auth/tokens'
179
180 creds = {
181 "auth": {
182 "identity": {
183 "methods": ["password"],
184 "password": {
185 "user": {
186 "name": user, "password": password,
187 "domain": {"id": domain_id}
188 }
189 }
190 },
191 "scope": {
192 "project": {
193 "domain": {"id": domain_id},
194 "name": project_name
195 }
196 }
197 }
198 }
199
200 headers = {'Content-Type': 'application/json'}
201 body = json.dumps(creds)
202 resp, body = self.http_obj.request(req_url, 'POST',
203 headers=headers, body=body)
204
205 if resp.status == 201:
206 try:
207 token = resp['x-subject-token']
208 except Exception:
209 self.LOG.exception("Failed to obtain token using V3"
210 " authentication (auth URL is '%s')" %
211 req_url)
212 raise
213
214 catalog = json.loads(body)['token']['catalog']
215
216 mgmt_url = None
217 for service_info in catalog:
218 if service_info['type'] != service:
219 continue # this isn't the entry for us.
220
221 endpoints = service_info['endpoints']
222
223 # Look for an endpoint in the region if configured.
224 if service in self.region:
225 region = self.region[service]
226
227 for ep in endpoints:
228 if ep['region'] != region:
229 continue
230
231 mgmt_url = ep['url']
232 # FIXME(blk-u): this isn't handling endpoint type
233 # (public, internal, admin).
234 break
235
236 if not mgmt_url:
237 # Didn't find endpoint for region, use the first.
238
239 ep = endpoints[0]
240 mgmt_url = ep['url']
241 # FIXME(blk-u): this isn't handling endpoint type
242 # (public, internal, admin).
243
244 break
245
246 return token, mgmt_url
247
248 elif resp.status == 401:
249 raise exceptions.AuthenticationFailure(user=user,
250 password=password)
251 else:
252 self.LOG.error("Failed to obtain token using V3 authentication"
253 " (auth URL is '%s'), the response status is %s" %
254 (req_url, resp.status))
255 raise exceptions.AuthenticationFailure(user=user,
256 password=password)
257
Daryl Walleck1465d612011-11-02 02:22:15 -0500258 def post(self, url, body, headers):
259 return self.request('POST', url, headers, body)
260
Attila Fazekasb8aa7592013-01-26 01:25:45 +0100261 def get(self, url, headers=None):
262 return self.request('GET', url, headers)
Daryl Walleck1465d612011-11-02 02:22:15 -0500263
Dan Smithba6cb162012-08-14 07:22:42 -0700264 def delete(self, url, headers=None):
265 return self.request('DELETE', url, headers)
Daryl Walleck1465d612011-11-02 02:22:15 -0500266
rajalakshmi-ganesanab426722013-02-08 15:49:15 +0530267 def patch(self, url, body, headers):
268 return self.request('PATCH', url, headers, body)
269
Daryl Walleck1465d612011-11-02 02:22:15 -0500270 def put(self, url, body, headers):
271 return self.request('PUT', url, headers, body)
272
dwalleck5d734432012-10-04 01:11:47 -0500273 def head(self, url, headers=None):
Larisa Ustalov6c3c7802012-11-05 12:25:19 +0200274 return self.request('HEAD', url, headers)
275
276 def copy(self, url, headers=None):
277 return self.request('COPY', url, headers)
dwalleck5d734432012-10-04 01:11:47 -0500278
Matthew Treinishc0f768f2013-03-11 14:24:16 -0400279 def get_versions(self):
280 resp, body = self.get('')
281 body = self._parse_resp(body)
282 body = body['versions']
283 versions = map(lambda x: x['id'], body)
284 return resp, versions
285
Attila Fazekas11d2a772013-01-29 17:46:52 +0100286 def _log_request(self, method, req_url, headers, body):
287 self.LOG.info('Request: ' + method + ' ' + req_url)
288 if headers:
289 print_headers = headers
290 if 'X-Auth-Token' in headers and headers['X-Auth-Token']:
291 token = headers['X-Auth-Token']
292 if len(token) > 64 and TOKEN_CHARS_RE.match(token):
293 print_headers = headers.copy()
294 print_headers['X-Auth-Token'] = "<Token omitted>"
295 self.LOG.debug('Request Headers: ' + str(print_headers))
296 if body:
297 str_body = str(body)
298 length = len(str_body)
299 self.LOG.debug('Request Body: ' + str_body[:2048])
300 if length >= 2048:
301 self.LOG.debug("Large body (%d) md5 summary: %s", length,
302 hashlib.md5(str_body).hexdigest())
303
304 def _log_response(self, resp, resp_body):
305 status = resp['status']
306 self.LOG.info("Response Status: " + status)
307 headers = resp.copy()
308 del headers['status']
Matthew Treinishe5423912013-08-13 18:07:31 -0400309 if headers.get('x-compute-request-id'):
310 self.LOG.info("Nova request id: %s" %
311 headers.pop('x-compute-request-id'))
312 elif headers.get('x-openstack-request-id'):
313 self.LOG.info("Glance request id %s" %
314 headers.pop('x-openstack-request-id'))
Attila Fazekas11d2a772013-01-29 17:46:52 +0100315 if len(headers):
316 self.LOG.debug('Response Headers: ' + str(headers))
317 if resp_body:
318 str_body = str(resp_body)
319 length = len(str_body)
320 self.LOG.debug('Response Body: ' + str_body[:2048])
321 if length >= 2048:
322 self.LOG.debug("Large body (%d) md5 summary: %s", length,
323 hashlib.md5(str_body).hexdigest())
Daryl Walleck8a707db2012-01-25 00:46:24 -0600324
Dan Smithba6cb162012-08-14 07:22:42 -0700325 def _parse_resp(self, body):
326 return json.loads(body)
327
Attila Fazekas836e4782013-01-29 15:40:13 +0100328 def response_checker(self, method, url, headers, body, resp, resp_body):
329 if (resp.status in set((204, 205, 304)) or resp.status < 200 or
Pavel Sedláke267eba2013-04-03 15:56:36 +0200330 method.upper() == 'HEAD') and resp_body:
Attila Fazekas836e4782013-01-29 15:40:13 +0100331 raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
332 #NOTE(afazekas):
333 # If the HTTP Status Code is 205
334 # 'The response MUST NOT include an entity.'
335 # A HTTP entity has an entity-body and an 'entity-header'.
336 # In the HTTP response specification (Section 6) the 'entity-header'
337 # 'generic-header' and 'response-header' are in OR relation.
338 # All headers not in the above two group are considered as entity
339 # header in every interpretation.
340
341 if (resp.status == 205 and
342 0 != len(set(resp.keys()) - set(('status',)) -
343 self.response_header_lc - self.general_header_lc)):
344 raise exceptions.ResponseWithEntity()
345 #NOTE(afazekas)
346 # Now the swift sometimes (delete not empty container)
347 # returns with non json error response, we can create new rest class
348 # for swift.
349 # Usually RFC2616 says error responses SHOULD contain an explanation.
350 # The warning is normal for SHOULD/SHOULD NOT case
351
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100352 # Likely it will cause an error
353 if not resp_body and resp.status >= 400:
Attila Fazekas11d2a772013-01-29 17:46:52 +0100354 self.LOG.warning("status >= 400 response with empty body")
Attila Fazekas836e4782013-01-29 15:40:13 +0100355
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100356 def _request(self, method, url,
357 headers=None, body=None):
Daryl Wallecke5b83d42011-11-10 14:39:02 -0600358 """A simple HTTP request interface."""
Daryl Walleck1465d612011-11-02 02:22:15 -0500359
Daryl Walleck1465d612011-11-02 02:22:15 -0500360 req_url = "%s/%s" % (self.base_url, url)
Attila Fazekas11d2a772013-01-29 17:46:52 +0100361 self._log_request(method, req_url, headers, body)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600362 resp, resp_body = self.http_obj.request(req_url, method,
Zhongyue Luo79d8d362012-09-25 13:49:27 +0800363 headers=headers, body=body)
Attila Fazekas11d2a772013-01-29 17:46:52 +0100364 self._log_response(resp, resp_body)
Attila Fazekas836e4782013-01-29 15:40:13 +0100365 self.response_checker(method, url, headers, body, resp, resp_body)
Attila Fazekas72c7a5f2012-12-03 17:17:23 +0100366
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100367 return resp, resp_body
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500368
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100369 def request(self, method, url,
370 headers=None, body=None):
371 retry = 0
372 if (self.token is None) or (self.base_url is None):
373 self._set_auth()
374
375 if headers is None:
376 headers = {}
377 headers['X-Auth-Token'] = self.token
378
379 resp, resp_body = self._request(method, url,
380 headers=headers, body=body)
381
382 while (resp.status == 413 and
383 'retry-after' in resp and
384 not self.is_absolute_limit(
385 resp, self._parse_resp(resp_body)) and
386 retry < MAX_RECURSION_DEPTH):
387 retry += 1
388 delay = int(resp['retry-after'])
389 time.sleep(delay)
390 resp, resp_body = self._request(method, url,
391 headers=headers, body=body)
392 self._error_checker(method, url, headers, body,
393 resp, resp_body)
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500394 return resp, resp_body
395
396 def _error_checker(self, method, url,
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100397 headers, body, resp, resp_body):
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500398
399 # NOTE(mtreinish): Check for httplib response from glance_http. The
400 # object can't be used here because importing httplib breaks httplib2.
401 # If another object from a class not imported were passed here as
402 # resp this could possibly fail
403 if str(type(resp)) == "<type 'instance'>":
404 ctype = resp.getheader('content-type')
405 else:
406 try:
407 ctype = resp['content-type']
408 # NOTE(mtreinish): Keystone delete user responses doesn't have a
409 # content-type header. (They don't have a body) So just pretend it
410 # is set.
411 except KeyError:
412 ctype = 'application/json'
413
Attila Fazekase72b7cd2013-03-26 18:34:21 +0100414 # It is not an error response
415 if resp.status < 400:
416 return
417
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500418 JSON_ENC = ['application/json; charset=UTF-8', 'application/json',
419 'application/json; charset=utf-8']
420 # NOTE(mtreinish): This is for compatibility with Glance and swift
421 # APIs. These are the return content types that Glance api v1
422 # (and occasionally swift) are using.
423 TXT_ENC = ['text/plain; charset=UTF-8', 'text/html; charset=UTF-8',
424 'text/plain; charset=utf-8']
425 XML_ENC = ['application/xml', 'application/xml; charset=UTF-8']
426
427 if ctype in JSON_ENC or ctype in XML_ENC:
428 parse_resp = True
429 elif ctype in TXT_ENC:
430 parse_resp = False
431 else:
432 raise exceptions.RestClientException(str(resp.status))
433
Rohit Karajgi6b1e1542012-05-14 05:55:54 -0700434 if resp.status == 401 or resp.status == 403:
Daryl Walleckced8eb82012-03-19 13:52:37 -0500435 raise exceptions.Unauthorized()
Jay Pipes5135bfc2012-01-05 15:46:49 -0500436
437 if resp.status == 404:
Daryl Walleck8a707db2012-01-25 00:46:24 -0600438 raise exceptions.NotFound(resp_body)
Jay Pipes5135bfc2012-01-05 15:46:49 -0500439
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600440 if resp.status == 400:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500441 if parse_resp:
442 resp_body = self._parse_resp(resp_body)
David Kranz28e35c52012-07-10 10:14:38 -0400443 raise exceptions.BadRequest(resp_body)
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600444
David Kranz5a23d862012-02-14 09:48:55 -0500445 if resp.status == 409:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500446 if parse_resp:
447 resp_body = self._parse_resp(resp_body)
David Kranz5a23d862012-02-14 09:48:55 -0500448 raise exceptions.Duplicate(resp_body)
449
Daryl Wallecked8bef32011-12-05 23:02:08 -0600450 if resp.status == 413:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500451 if parse_resp:
452 resp_body = self._parse_resp(resp_body)
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100453 if self.is_absolute_limit(resp, resp_body):
454 raise exceptions.OverLimit(resp_body)
455 else:
456 raise exceptions.RateLimitExceeded(resp_body)
Brian Lamar12d9b292011-12-08 12:41:21 -0500457
Wangpana9b54c62013-02-28 11:04:32 +0800458 if resp.status == 422:
459 if parse_resp:
460 resp_body = self._parse_resp(resp_body)
461 raise exceptions.UnprocessableEntity(resp_body)
462
Daryl Wallecked8bef32011-12-05 23:02:08 -0600463 if resp.status in (500, 501):
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500464 message = resp_body
465 if parse_resp:
466 resp_body = self._parse_resp(resp_body)
467 #I'm seeing both computeFault and cloudServersFault come back.
468 #Will file a bug to fix, but leave as is for now.
469 if 'cloudServersFault' in resp_body:
470 message = resp_body['cloudServersFault']['message']
471 elif 'computeFault' in resp_body:
472 message = resp_body['computeFault']['message']
473 elif 'error' in resp_body: # Keystone errors
474 message = resp_body['error']['message']
475 raise exceptions.IdentityError(message)
476 elif 'message' in resp_body:
477 message = resp_body['message']
Dan Princea4b709c2012-10-10 12:27:59 -0400478
Daryl Walleckf0087032011-12-18 13:37:05 -0600479 raise exceptions.ComputeFault(message)
Daryl Wallecked8bef32011-12-05 23:02:08 -0600480
David Kranz5a23d862012-02-14 09:48:55 -0500481 if resp.status >= 400:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500482 if parse_resp:
483 resp_body = self._parse_resp(resp_body)
Attila Fazekas96524032013-01-29 19:52:49 +0100484 raise exceptions.RestClientException(str(resp.status))
David Kranz5a23d862012-02-14 09:48:55 -0500485
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100486 def is_absolute_limit(self, resp, resp_body):
487 if (not isinstance(resp_body, collections.Mapping) or
Pavel Sedláke267eba2013-04-03 15:56:36 +0200488 'retry-after' not in resp):
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100489 return True
490 over_limit = resp_body.get('overLimit', None)
491 if not over_limit:
492 return True
493 return 'exceed' in over_limit.get('message', 'blabla')
rajalakshmi-ganesan0275a0d2013-01-11 18:26:05 +0530494
David Kranz6aceb4a2012-06-05 14:05:45 -0400495 def wait_for_resource_deletion(self, id):
Sean Daguef237ccb2013-01-04 15:19:14 -0500496 """Waits for a resource to be deleted."""
David Kranz6aceb4a2012-06-05 14:05:45 -0400497 start_time = int(time.time())
498 while True:
499 if self.is_resource_deleted(id):
500 return
501 if int(time.time()) - start_time >= self.build_timeout:
502 raise exceptions.TimeoutException
503 time.sleep(self.build_interval)
504
505 def is_resource_deleted(self, id):
506 """
507 Subclasses override with specific deletion detection.
508 """
Attila Fazekasd236b4e2013-01-26 00:44:12 +0100509 message = ('"%s" does not implement is_resource_deleted'
510 % self.__class__.__name__)
511 raise NotImplementedError(message)
Dan Smithba6cb162012-08-14 07:22:42 -0700512
513
514class RestClientXML(RestClient):
515 TYPE = "xml"
516
517 def _parse_resp(self, body):
518 return xml_to_json(etree.fromstring(body))
rajalakshmi-ganesan0275a0d2013-01-11 18:26:05 +0530519
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100520 def is_absolute_limit(self, resp, resp_body):
521 if (not isinstance(resp_body, collections.Mapping) or
Pavel Sedláke267eba2013-04-03 15:56:36 +0200522 'retry-after' not in resp):
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100523 return True
524 return 'exceed' in resp_body.get('message', 'blabla')