blob: 170a13718c011e39c691946ad10a9243c141c3ce [file] [log] [blame]
Jay Pipes3f981df2012-03-27 18:59:44 -04001# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2012 OpenStack, LLC
4# All Rights Reserved.
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
Attila Fazekas11d2a772013-01-29 17:46:52 +010018import hashlib
Jay Pipes7f757632011-12-02 15:53:32 -050019import httplib2
Matthew Treinisha83a16e2012-12-07 13:44:02 -050020import json
Daryl Walleck8a707db2012-01-25 00:46:24 -060021import logging
Dan Smithba6cb162012-08-14 07:22:42 -070022from lxml import etree
Attila Fazekas11d2a772013-01-29 17:46:52 +010023import re
Eoghan Glynna5598972012-03-01 09:27:17 -050024import time
Jay Pipes3f981df2012-03-27 18:59:44 -040025
Daryl Wallecked8bef32011-12-05 23:02:08 -060026from tempest import exceptions
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
33
Daryl Walleck1465d612011-11-02 02:22:15 -050034class RestClient(object):
Dan Smithba6cb162012-08-14 07:22:42 -070035 TYPE = "json"
Attila Fazekas11d2a772013-01-29 17:46:52 +010036 LOG = logging.getLogger(__name__)
Daryl Walleck1465d612011-11-02 02:22:15 -050037
chris fattarsi5098fa22012-04-17 13:27:00 -070038 def __init__(self, config, user, password, auth_url, tenant_name=None):
Jay Pipes7f757632011-12-02 15:53:32 -050039 self.config = config
chris fattarsi5098fa22012-04-17 13:27:00 -070040 self.user = user
41 self.password = password
42 self.auth_url = auth_url
43 self.tenant_name = tenant_name
44
45 self.service = None
46 self.token = None
47 self.base_url = None
Attila Fazekascadcb1f2013-01-21 23:10:53 +010048 self.region = {'compute': self.config.identity.region}
chris fattarsi5098fa22012-04-17 13:27:00 -070049 self.endpoint_url = 'publicURL'
50 self.strategy = self.config.identity.strategy
Dan Smithba6cb162012-08-14 07:22:42 -070051 self.headers = {'Content-Type': 'application/%s' % self.TYPE,
52 'Accept': 'application/%s' % self.TYPE}
David Kranz6aceb4a2012-06-05 14:05:45 -040053 self.build_interval = config.compute.build_interval
54 self.build_timeout = config.compute.build_timeout
Attila Fazekas72c7a5f2012-12-03 17:17:23 +010055 self.general_header_lc = set(('cache-control', 'connection',
56 'date', 'pragma', 'trailer',
57 'transfer-encoding', 'via',
58 'warning'))
59 self.response_header_lc = set(('accept-ranges', 'age', 'etag',
60 'location', 'proxy-authenticate',
61 'retry-after', 'server',
62 'vary', 'www-authenticate'))
chris fattarsi5098fa22012-04-17 13:27:00 -070063
64 def _set_auth(self):
65 """
66 Sets the token and base_url used in requests based on the strategy type
67 """
68
69 if self.strategy == 'keystone':
70 self.token, self.base_url = self.keystone_auth(self.user,
71 self.password,
72 self.auth_url,
73 self.service,
74 self.tenant_name)
Daryl Walleck1465d612011-11-02 02:22:15 -050075 else:
chris fattarsi5098fa22012-04-17 13:27:00 -070076 self.token, self.base_url = self.basic_auth(self.user,
77 self.password,
78 self.auth_url)
79
80 def clear_auth(self):
81 """
82 Can be called to clear the token and base_url so that the next request
Attila Fazekasb2902af2013-02-16 16:22:44 +010083 will fetch a new token and base_url.
chris fattarsi5098fa22012-04-17 13:27:00 -070084 """
85
86 self.token = None
87 self.base_url = None
Daryl Walleck1465d612011-11-02 02:22:15 -050088
Rohit Karajgi6b1e1542012-05-14 05:55:54 -070089 def get_auth(self):
90 """Returns the token of the current request or sets the token if
Attila Fazekasb2902af2013-02-16 16:22:44 +010091 none.
92 """
Rohit Karajgi6b1e1542012-05-14 05:55:54 -070093
94 if not self.token:
95 self._set_auth()
96
97 return self.token
98
Daryl Walleck587385b2012-03-03 13:00:26 -060099 def basic_auth(self, user, password, auth_url):
Daryl Walleck1465d612011-11-02 02:22:15 -0500100 """
Attila Fazekasb2902af2013-02-16 16:22:44 +0100101 Provides authentication for the target API.
Daryl Walleck1465d612011-11-02 02:22:15 -0500102 """
103
104 params = {}
105 params['headers'] = {'User-Agent': 'Test-Client', 'X-Auth-User': user,
Daryl Walleck587385b2012-03-03 13:00:26 -0600106 'X-Auth-Key': password}
Daryl Walleck1465d612011-11-02 02:22:15 -0500107
Jay Pipescd8eaec2013-01-16 21:03:48 -0500108 dscv = self.config.identity.disable_ssl_certificate_validation
109 self.http_obj = httplib2.Http(disable_ssl_certificate_validation=dscv)
Daryl Walleck1465d612011-11-02 02:22:15 -0500110 resp, body = self.http_obj.request(auth_url, 'GET', **params)
111 try:
112 return resp['x-auth-token'], resp['x-server-management-url']
Matthew Treinish05d9fb92012-12-07 16:14:05 -0500113 except Exception:
Daryl Walleck1465d612011-11-02 02:22:15 -0500114 raise
115
Daryl Walleck587385b2012-03-03 13:00:26 -0600116 def keystone_auth(self, user, password, auth_url, service, tenant_name):
Daryl Walleck1465d612011-11-02 02:22:15 -0500117 """
Attila Fazekasb2902af2013-02-16 16:22:44 +0100118 Provides authentication via Keystone.
Daryl Walleck1465d612011-11-02 02:22:15 -0500119 """
120
Jay Pipes7c88eb22013-01-16 21:32:43 -0500121 # Normalize URI to ensure /tokens is in it.
122 if 'tokens' not in auth_url:
123 auth_url = auth_url.rstrip('/') + '/tokens'
124
Zhongyue Luo30a563f2012-09-30 23:43:50 +0900125 creds = {
126 'auth': {
Daryl Walleck1465d612011-11-02 02:22:15 -0500127 'passwordCredentials': {
128 'username': user,
Daryl Walleck587385b2012-03-03 13:00:26 -0600129 'password': password,
Daryl Walleck1465d612011-11-02 02:22:15 -0500130 },
Zhongyue Luo30a563f2012-09-30 23:43:50 +0900131 'tenantName': tenant_name,
Daryl Walleck1465d612011-11-02 02:22:15 -0500132 }
133 }
134
Jay Pipescd8eaec2013-01-16 21:03:48 -0500135 dscv = self.config.identity.disable_ssl_certificate_validation
136 self.http_obj = httplib2.Http(disable_ssl_certificate_validation=dscv)
Daryl Walleck1465d612011-11-02 02:22:15 -0500137 headers = {'Content-Type': 'application/json'}
138 body = json.dumps(creds)
139 resp, body = self.http_obj.request(auth_url, 'POST',
140 headers=headers, body=body)
141
Jay Pipes7f757632011-12-02 15:53:32 -0500142 if resp.status == 200:
143 try:
144 auth_data = json.loads(body)['access']
145 token = auth_data['token']['id']
Jay Pipes7f757632011-12-02 15:53:32 -0500146 except Exception, e:
Adam Gandelmane2d46b42012-01-03 17:40:44 -0800147 print "Failed to obtain token for user: %s" % e
Jay Pipes7f757632011-12-02 15:53:32 -0500148 raise
Adam Gandelmane2d46b42012-01-03 17:40:44 -0800149
150 mgmt_url = None
151 for ep in auth_data['serviceCatalog']:
Dan Prince8527c8a2012-12-14 14:00:31 -0500152 if ep["type"] == service:
K Jonathan Harkerd6ba4b42012-12-18 13:50:47 -0800153 for _ep in ep['endpoints']:
154 if service in self.region and \
155 _ep['region'] == self.region[service]:
156 mgmt_url = _ep[self.endpoint_url]
157 if not mgmt_url:
158 mgmt_url = ep['endpoints'][0][self.endpoint_url]
Rohit Karajgid2a28af2012-05-23 03:44:59 -0700159 tenant_id = auth_data['token']['tenant']['id']
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 if service == 'network':
166 # Keystone does not return the correct endpoint for
167 # quantum. Handle this separately.
Zhongyue Luoe0884a32012-09-25 17:24:17 +0800168 mgmt_url = (mgmt_url + self.config.network.api_version +
169 "/tenants/" + tenant_id)
Rohit Karajgid2a28af2012-05-23 03:44:59 -0700170
171 return token, mgmt_url
172
Jay Pipes7f757632011-12-02 15:53:32 -0500173 elif resp.status == 401:
Daryl Wallecka22f57b2012-03-20 16:52:07 -0500174 raise exceptions.AuthenticationFailure(user=user,
175 password=password)
Daryl Walleck1465d612011-11-02 02:22:15 -0500176
177 def post(self, url, body, headers):
178 return self.request('POST', url, headers, body)
179
Matthew Treinish426326e2012-11-30 13:17:00 -0500180 def get(self, url, headers=None, wait=None):
181 return self.request('GET', url, headers, wait=wait)
Daryl Walleck1465d612011-11-02 02:22:15 -0500182
Dan Smithba6cb162012-08-14 07:22:42 -0700183 def delete(self, url, headers=None):
184 return self.request('DELETE', url, headers)
Daryl Walleck1465d612011-11-02 02:22:15 -0500185
186 def put(self, url, body, headers):
187 return self.request('PUT', url, headers, body)
188
dwalleck5d734432012-10-04 01:11:47 -0500189 def head(self, url, headers=None):
Larisa Ustalov6c3c7802012-11-05 12:25:19 +0200190 return self.request('HEAD', url, headers)
191
192 def copy(self, url, headers=None):
193 return self.request('COPY', url, headers)
dwalleck5d734432012-10-04 01:11:47 -0500194
Attila Fazekas11d2a772013-01-29 17:46:52 +0100195 def _log_request(self, method, req_url, headers, body):
196 self.LOG.info('Request: ' + method + ' ' + req_url)
197 if headers:
198 print_headers = headers
199 if 'X-Auth-Token' in headers and headers['X-Auth-Token']:
200 token = headers['X-Auth-Token']
201 if len(token) > 64 and TOKEN_CHARS_RE.match(token):
202 print_headers = headers.copy()
203 print_headers['X-Auth-Token'] = "<Token omitted>"
204 self.LOG.debug('Request Headers: ' + str(print_headers))
205 if body:
206 str_body = str(body)
207 length = len(str_body)
208 self.LOG.debug('Request Body: ' + str_body[:2048])
209 if length >= 2048:
210 self.LOG.debug("Large body (%d) md5 summary: %s", length,
211 hashlib.md5(str_body).hexdigest())
212
213 def _log_response(self, resp, resp_body):
214 status = resp['status']
215 self.LOG.info("Response Status: " + status)
216 headers = resp.copy()
217 del headers['status']
218 if len(headers):
219 self.LOG.debug('Response Headers: ' + str(headers))
220 if resp_body:
221 str_body = str(resp_body)
222 length = len(str_body)
223 self.LOG.debug('Response Body: ' + str_body[:2048])
224 if length >= 2048:
225 self.LOG.debug("Large body (%d) md5 summary: %s", length,
226 hashlib.md5(str_body).hexdigest())
Daryl Walleck8a707db2012-01-25 00:46:24 -0600227
Dan Smithba6cb162012-08-14 07:22:42 -0700228 def _parse_resp(self, body):
229 return json.loads(body)
230
Attila Fazekas836e4782013-01-29 15:40:13 +0100231 def response_checker(self, method, url, headers, body, resp, resp_body):
232 if (resp.status in set((204, 205, 304)) or resp.status < 200 or
233 method.upper() == 'HEAD') and resp_body:
234 raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
235 #NOTE(afazekas):
236 # If the HTTP Status Code is 205
237 # 'The response MUST NOT include an entity.'
238 # A HTTP entity has an entity-body and an 'entity-header'.
239 # In the HTTP response specification (Section 6) the 'entity-header'
240 # 'generic-header' and 'response-header' are in OR relation.
241 # All headers not in the above two group are considered as entity
242 # header in every interpretation.
243
244 if (resp.status == 205 and
245 0 != len(set(resp.keys()) - set(('status',)) -
246 self.response_header_lc - self.general_header_lc)):
247 raise exceptions.ResponseWithEntity()
248 #NOTE(afazekas)
249 # Now the swift sometimes (delete not empty container)
250 # returns with non json error response, we can create new rest class
251 # for swift.
252 # Usually RFC2616 says error responses SHOULD contain an explanation.
253 # The warning is normal for SHOULD/SHOULD NOT case
254
255 # Likely it will cause error
256 if not body and resp.status >= 400:
Attila Fazekas11d2a772013-01-29 17:46:52 +0100257 self.LOG.warning("status >= 400 response with empty body")
Attila Fazekas836e4782013-01-29 15:40:13 +0100258
Matthew Treinish426326e2012-11-30 13:17:00 -0500259 def request(self, method, url,
260 headers=None, body=None, depth=0, wait=None):
Daryl Wallecke5b83d42011-11-10 14:39:02 -0600261 """A simple HTTP request interface."""
Daryl Walleck1465d612011-11-02 02:22:15 -0500262
chris fattarsi5098fa22012-04-17 13:27:00 -0700263 if (self.token is None) or (self.base_url is None):
264 self._set_auth()
265
Jay Pipescd8eaec2013-01-16 21:03:48 -0500266 dscv = self.config.identity.disable_ssl_certificate_validation
267 self.http_obj = httplib2.Http(disable_ssl_certificate_validation=dscv)
Zhongyue Luoe471d6e2012-09-17 17:02:43 +0800268 if headers is None:
Daryl Walleck1465d612011-11-02 02:22:15 -0500269 headers = {}
270 headers['X-Auth-Token'] = self.token
271
272 req_url = "%s/%s" % (self.base_url, url)
Attila Fazekas11d2a772013-01-29 17:46:52 +0100273 self._log_request(method, req_url, headers, body)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600274 resp, resp_body = self.http_obj.request(req_url, method,
Zhongyue Luo79d8d362012-09-25 13:49:27 +0800275 headers=headers, body=body)
Attila Fazekas11d2a772013-01-29 17:46:52 +0100276 self._log_response(resp, resp_body)
Attila Fazekas836e4782013-01-29 15:40:13 +0100277 self.response_checker(method, url, headers, body, resp, resp_body)
Attila Fazekas72c7a5f2012-12-03 17:17:23 +0100278
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500279 self._error_checker(method, url, headers, body, resp, resp_body, depth,
280 wait)
281
282 return resp, resp_body
283
284 def _error_checker(self, method, url,
285 headers, body, resp, resp_body, depth=0, wait=None):
286
287 # NOTE(mtreinish): Check for httplib response from glance_http. The
288 # object can't be used here because importing httplib breaks httplib2.
289 # If another object from a class not imported were passed here as
290 # resp this could possibly fail
291 if str(type(resp)) == "<type 'instance'>":
292 ctype = resp.getheader('content-type')
293 else:
294 try:
295 ctype = resp['content-type']
296 # NOTE(mtreinish): Keystone delete user responses doesn't have a
297 # content-type header. (They don't have a body) So just pretend it
298 # is set.
299 except KeyError:
300 ctype = 'application/json'
301
302 JSON_ENC = ['application/json; charset=UTF-8', 'application/json',
303 'application/json; charset=utf-8']
304 # NOTE(mtreinish): This is for compatibility with Glance and swift
305 # APIs. These are the return content types that Glance api v1
306 # (and occasionally swift) are using.
307 TXT_ENC = ['text/plain; charset=UTF-8', 'text/html; charset=UTF-8',
308 'text/plain; charset=utf-8']
309 XML_ENC = ['application/xml', 'application/xml; charset=UTF-8']
310
311 if ctype in JSON_ENC or ctype in XML_ENC:
312 parse_resp = True
313 elif ctype in TXT_ENC:
314 parse_resp = False
315 else:
316 raise exceptions.RestClientException(str(resp.status))
317
Rohit Karajgi6b1e1542012-05-14 05:55:54 -0700318 if resp.status == 401 or resp.status == 403:
Daryl Walleckced8eb82012-03-19 13:52:37 -0500319 raise exceptions.Unauthorized()
Jay Pipes5135bfc2012-01-05 15:46:49 -0500320
321 if resp.status == 404:
Daryl Walleck8a707db2012-01-25 00:46:24 -0600322 raise exceptions.NotFound(resp_body)
Jay Pipes5135bfc2012-01-05 15:46:49 -0500323
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600324 if resp.status == 400:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500325 if parse_resp:
326 resp_body = self._parse_resp(resp_body)
David Kranz28e35c52012-07-10 10:14:38 -0400327 raise exceptions.BadRequest(resp_body)
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600328
David Kranz5a23d862012-02-14 09:48:55 -0500329 if resp.status == 409:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500330 if parse_resp:
331 resp_body = self._parse_resp(resp_body)
David Kranz5a23d862012-02-14 09:48:55 -0500332 raise exceptions.Duplicate(resp_body)
333
Daryl Wallecked8bef32011-12-05 23:02:08 -0600334 if resp.status == 413:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500335 if parse_resp:
336 resp_body = self._parse_resp(resp_body)
rajalakshmi-ganesan0275a0d2013-01-11 18:26:05 +0530337 #Checking whether Absolute/Rate limit
338 return self.check_over_limit(resp_body, method, url, headers, body,
339 depth, wait)
Brian Lamar12d9b292011-12-08 12:41:21 -0500340
Daryl Wallecked8bef32011-12-05 23:02:08 -0600341 if resp.status in (500, 501):
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500342 message = resp_body
343 if parse_resp:
344 resp_body = self._parse_resp(resp_body)
345 #I'm seeing both computeFault and cloudServersFault come back.
346 #Will file a bug to fix, but leave as is for now.
347 if 'cloudServersFault' in resp_body:
348 message = resp_body['cloudServersFault']['message']
349 elif 'computeFault' in resp_body:
350 message = resp_body['computeFault']['message']
351 elif 'error' in resp_body: # Keystone errors
352 message = resp_body['error']['message']
353 raise exceptions.IdentityError(message)
354 elif 'message' in resp_body:
355 message = resp_body['message']
Dan Princea4b709c2012-10-10 12:27:59 -0400356
Daryl Walleckf0087032011-12-18 13:37:05 -0600357 raise exceptions.ComputeFault(message)
Daryl Wallecked8bef32011-12-05 23:02:08 -0600358
David Kranz5a23d862012-02-14 09:48:55 -0500359 if resp.status >= 400:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500360 if parse_resp:
361 resp_body = self._parse_resp(resp_body)
Attila Fazekas96524032013-01-29 19:52:49 +0100362 raise exceptions.RestClientException(str(resp.status))
David Kranz5a23d862012-02-14 09:48:55 -0500363
rajalakshmi-ganesan0275a0d2013-01-11 18:26:05 +0530364 def check_over_limit(self, resp_body, method, url,
365 headers, body, depth, wait):
366 self.is_absolute_limit(resp_body['overLimit'])
367 return self.is_rate_limit_retry_max_recursion_depth(
368 resp_body['overLimit'], method, url, headers,
369 body, depth, wait)
370
371 def is_absolute_limit(self, resp_body):
372 if 'exceeded' in resp_body['message']:
373 raise exceptions.OverLimit(resp_body['message'])
374 else:
375 return
376
377 def is_rate_limit_retry_max_recursion_depth(self, resp_body, method,
378 url, headers, body, depth,
379 wait):
380 if 'retryAfter' in resp_body:
381 if depth < MAX_RECURSION_DEPTH:
382 delay = resp_body['retryAfter']
383 time.sleep(int(delay))
384 return self.request(method, url, headers=headers,
385 body=body,
386 depth=depth + 1, wait=wait)
387 else:
388 raise exceptions.RateLimitExceeded(
389 message=resp_body['overLimitFault']['message'],
390 details=resp_body['overLimitFault']['details'])
391 else:
392 raise exceptions.OverLimit(resp_body['message'])
393
David Kranz6aceb4a2012-06-05 14:05:45 -0400394 def wait_for_resource_deletion(self, id):
Sean Daguef237ccb2013-01-04 15:19:14 -0500395 """Waits for a resource to be deleted."""
David Kranz6aceb4a2012-06-05 14:05:45 -0400396 start_time = int(time.time())
397 while True:
398 if self.is_resource_deleted(id):
399 return
400 if int(time.time()) - start_time >= self.build_timeout:
401 raise exceptions.TimeoutException
402 time.sleep(self.build_interval)
403
404 def is_resource_deleted(self, id):
405 """
406 Subclasses override with specific deletion detection.
407 """
Attila Fazekasd236b4e2013-01-26 00:44:12 +0100408 message = ('"%s" does not implement is_resource_deleted'
409 % self.__class__.__name__)
410 raise NotImplementedError(message)
Dan Smithba6cb162012-08-14 07:22:42 -0700411
412
413class RestClientXML(RestClient):
414 TYPE = "xml"
415
416 def _parse_resp(self, body):
417 return xml_to_json(etree.fromstring(body))
rajalakshmi-ganesan0275a0d2013-01-11 18:26:05 +0530418
419 def check_over_limit(self, resp_body, method, url,
420 headers, body, depth, wait):
421 self.is_absolute_limit(resp_body)
422 return self.is_rate_limit_retry_max_recursion_depth(
423 resp_body, method, url, headers,
424 body, depth, wait)