blob: 8fc554523e88ab121a25210a71b48a8e723612be [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
Jay Pipes7f757632011-12-02 15:53:32 -050018import httplib2
Matthew Treinisha83a16e2012-12-07 13:44:02 -050019import json
Daryl Walleck8a707db2012-01-25 00:46:24 -060020import logging
Dan Smithba6cb162012-08-14 07:22:42 -070021from lxml import etree
Eoghan Glynna5598972012-03-01 09:27:17 -050022import time
Jay Pipes3f981df2012-03-27 18:59:44 -040023
Daryl Wallecked8bef32011-12-05 23:02:08 -060024from tempest import exceptions
dwallecke62b9f02012-10-10 23:34:42 -050025from tempest.services.compute.xml.common import xml_to_json
Daryl Walleck1465d612011-11-02 02:22:15 -050026
Eoghan Glynna5598972012-03-01 09:27:17 -050027# redrive rate limited calls at most twice
28MAX_RECURSION_DEPTH = 2
29
30
Daryl Walleck1465d612011-11-02 02:22:15 -050031class RestClient(object):
Dan Smithba6cb162012-08-14 07:22:42 -070032 TYPE = "json"
Daryl Walleck1465d612011-11-02 02:22:15 -050033
chris fattarsi5098fa22012-04-17 13:27:00 -070034 def __init__(self, config, user, password, auth_url, tenant_name=None):
Daryl Walleck8a707db2012-01-25 00:46:24 -060035 self.log = logging.getLogger(__name__)
David Kranz180fed12012-03-27 14:31:29 -040036 self.log.setLevel(getattr(logging, config.compute.log_level))
Jay Pipes7f757632011-12-02 15:53:32 -050037 self.config = config
chris fattarsi5098fa22012-04-17 13:27:00 -070038 self.user = user
39 self.password = password
40 self.auth_url = auth_url
41 self.tenant_name = tenant_name
42
43 self.service = None
44 self.token = None
45 self.base_url = None
46 self.config = config
Attila Fazekascadcb1f2013-01-21 23:10:53 +010047 self.region = {'compute': self.config.identity.region}
chris fattarsi5098fa22012-04-17 13:27:00 -070048 self.endpoint_url = 'publicURL'
49 self.strategy = self.config.identity.strategy
Dan Smithba6cb162012-08-14 07:22:42 -070050 self.headers = {'Content-Type': 'application/%s' % self.TYPE,
51 'Accept': 'application/%s' % self.TYPE}
David Kranz6aceb4a2012-06-05 14:05:45 -040052 self.build_interval = config.compute.build_interval
53 self.build_timeout = config.compute.build_timeout
Attila Fazekas72c7a5f2012-12-03 17:17:23 +010054 self.general_header_lc = set(('cache-control', 'connection',
55 'date', 'pragma', 'trailer',
56 'transfer-encoding', 'via',
57 'warning'))
58 self.response_header_lc = set(('accept-ranges', 'age', 'etag',
59 'location', 'proxy-authenticate',
60 'retry-after', 'server',
61 'vary', 'www-authenticate'))
chris fattarsi5098fa22012-04-17 13:27:00 -070062
63 def _set_auth(self):
64 """
65 Sets the token and base_url used in requests based on the strategy type
66 """
67
68 if self.strategy == 'keystone':
69 self.token, self.base_url = self.keystone_auth(self.user,
70 self.password,
71 self.auth_url,
72 self.service,
73 self.tenant_name)
Daryl Walleck1465d612011-11-02 02:22:15 -050074 else:
chris fattarsi5098fa22012-04-17 13:27:00 -070075 self.token, self.base_url = self.basic_auth(self.user,
76 self.password,
77 self.auth_url)
78
79 def clear_auth(self):
80 """
81 Can be called to clear the token and base_url so that the next request
82 will fetch a new token and base_url
83 """
84
85 self.token = None
86 self.base_url = None
Daryl Walleck1465d612011-11-02 02:22:15 -050087
Rohit Karajgi6b1e1542012-05-14 05:55:54 -070088 def get_auth(self):
89 """Returns the token of the current request or sets the token if
90 none"""
91
92 if not self.token:
93 self._set_auth()
94
95 return self.token
96
Daryl Walleck587385b2012-03-03 13:00:26 -060097 def basic_auth(self, user, password, auth_url):
Daryl Walleck1465d612011-11-02 02:22:15 -050098 """
99 Provides authentication for the target API
100 """
101
102 params = {}
103 params['headers'] = {'User-Agent': 'Test-Client', 'X-Auth-User': user,
Daryl Walleck587385b2012-03-03 13:00:26 -0600104 'X-Auth-Key': password}
Daryl Walleck1465d612011-11-02 02:22:15 -0500105
Jay Pipescd8eaec2013-01-16 21:03:48 -0500106 dscv = self.config.identity.disable_ssl_certificate_validation
107 self.http_obj = httplib2.Http(disable_ssl_certificate_validation=dscv)
Daryl Walleck1465d612011-11-02 02:22:15 -0500108 resp, body = self.http_obj.request(auth_url, 'GET', **params)
109 try:
110 return resp['x-auth-token'], resp['x-server-management-url']
Matthew Treinish05d9fb92012-12-07 16:14:05 -0500111 except Exception:
Daryl Walleck1465d612011-11-02 02:22:15 -0500112 raise
113
Daryl Walleck587385b2012-03-03 13:00:26 -0600114 def keystone_auth(self, user, password, auth_url, service, tenant_name):
Daryl Walleck1465d612011-11-02 02:22:15 -0500115 """
Daryl Walleck587385b2012-03-03 13:00:26 -0600116 Provides authentication via Keystone
Daryl Walleck1465d612011-11-02 02:22:15 -0500117 """
118
Jay Pipes7c88eb22013-01-16 21:32:43 -0500119 # Normalize URI to ensure /tokens is in it.
120 if 'tokens' not in auth_url:
121 auth_url = auth_url.rstrip('/') + '/tokens'
122
Zhongyue Luo30a563f2012-09-30 23:43:50 +0900123 creds = {
124 'auth': {
Daryl Walleck1465d612011-11-02 02:22:15 -0500125 'passwordCredentials': {
126 'username': user,
Daryl Walleck587385b2012-03-03 13:00:26 -0600127 'password': password,
Daryl Walleck1465d612011-11-02 02:22:15 -0500128 },
Zhongyue Luo30a563f2012-09-30 23:43:50 +0900129 'tenantName': tenant_name,
Daryl Walleck1465d612011-11-02 02:22:15 -0500130 }
131 }
132
Jay Pipescd8eaec2013-01-16 21:03:48 -0500133 dscv = self.config.identity.disable_ssl_certificate_validation
134 self.http_obj = httplib2.Http(disable_ssl_certificate_validation=dscv)
Daryl Walleck1465d612011-11-02 02:22:15 -0500135 headers = {'Content-Type': 'application/json'}
136 body = json.dumps(creds)
137 resp, body = self.http_obj.request(auth_url, 'POST',
138 headers=headers, body=body)
139
Jay Pipes7f757632011-12-02 15:53:32 -0500140 if resp.status == 200:
141 try:
142 auth_data = json.loads(body)['access']
143 token = auth_data['token']['id']
Jay Pipes7f757632011-12-02 15:53:32 -0500144 except Exception, e:
Adam Gandelmane2d46b42012-01-03 17:40:44 -0800145 print "Failed to obtain token for user: %s" % e
Jay Pipes7f757632011-12-02 15:53:32 -0500146 raise
Adam Gandelmane2d46b42012-01-03 17:40:44 -0800147
148 mgmt_url = None
149 for ep in auth_data['serviceCatalog']:
Dan Prince8527c8a2012-12-14 14:00:31 -0500150 if ep["type"] == service:
K Jonathan Harkerd6ba4b42012-12-18 13:50:47 -0800151 for _ep in ep['endpoints']:
152 if service in self.region and \
153 _ep['region'] == self.region[service]:
154 mgmt_url = _ep[self.endpoint_url]
155 if not mgmt_url:
156 mgmt_url = ep['endpoints'][0][self.endpoint_url]
Rohit Karajgid2a28af2012-05-23 03:44:59 -0700157 tenant_id = auth_data['token']['tenant']['id']
Adam Gandelmane2d46b42012-01-03 17:40:44 -0800158 break
159
Zhongyue Luoe471d6e2012-09-17 17:02:43 +0800160 if mgmt_url is None:
Adam Gandelmane2d46b42012-01-03 17:40:44 -0800161 raise exceptions.EndpointNotFound(service)
162
Rohit Karajgid2a28af2012-05-23 03:44:59 -0700163 if service == 'network':
164 # Keystone does not return the correct endpoint for
165 # quantum. Handle this separately.
Zhongyue Luoe0884a32012-09-25 17:24:17 +0800166 mgmt_url = (mgmt_url + self.config.network.api_version +
167 "/tenants/" + tenant_id)
Rohit Karajgid2a28af2012-05-23 03:44:59 -0700168
169 return token, mgmt_url
170
Jay Pipes7f757632011-12-02 15:53:32 -0500171 elif resp.status == 401:
Daryl Wallecka22f57b2012-03-20 16:52:07 -0500172 raise exceptions.AuthenticationFailure(user=user,
173 password=password)
Daryl Walleck1465d612011-11-02 02:22:15 -0500174
175 def post(self, url, body, headers):
176 return self.request('POST', url, headers, body)
177
Matthew Treinish426326e2012-11-30 13:17:00 -0500178 def get(self, url, headers=None, wait=None):
179 return self.request('GET', url, headers, wait=wait)
Daryl Walleck1465d612011-11-02 02:22:15 -0500180
Dan Smithba6cb162012-08-14 07:22:42 -0700181 def delete(self, url, headers=None):
182 return self.request('DELETE', url, headers)
Daryl Walleck1465d612011-11-02 02:22:15 -0500183
184 def put(self, url, body, headers):
185 return self.request('PUT', url, headers, body)
186
dwalleck5d734432012-10-04 01:11:47 -0500187 def head(self, url, headers=None):
Larisa Ustalov6c3c7802012-11-05 12:25:19 +0200188 return self.request('HEAD', url, headers)
189
190 def copy(self, url, headers=None):
191 return self.request('COPY', url, headers)
dwalleck5d734432012-10-04 01:11:47 -0500192
Daryl Walleck8a707db2012-01-25 00:46:24 -0600193 def _log(self, req_url, body, resp, resp_body):
194 self.log.error('Request URL: ' + req_url)
195 self.log.error('Request Body: ' + str(body))
196 self.log.error('Response Headers: ' + str(resp))
197 self.log.error('Response Body: ' + str(resp_body))
198
Dan Smithba6cb162012-08-14 07:22:42 -0700199 def _parse_resp(self, body):
200 return json.loads(body)
201
Matthew Treinish426326e2012-11-30 13:17:00 -0500202 def request(self, method, url,
203 headers=None, body=None, depth=0, wait=None):
Daryl Wallecke5b83d42011-11-10 14:39:02 -0600204 """A simple HTTP request interface."""
Daryl Walleck1465d612011-11-02 02:22:15 -0500205
chris fattarsi5098fa22012-04-17 13:27:00 -0700206 if (self.token is None) or (self.base_url is None):
207 self._set_auth()
208
Jay Pipescd8eaec2013-01-16 21:03:48 -0500209 dscv = self.config.identity.disable_ssl_certificate_validation
210 self.http_obj = httplib2.Http(disable_ssl_certificate_validation=dscv)
Zhongyue Luoe471d6e2012-09-17 17:02:43 +0800211 if headers is None:
Daryl Walleck1465d612011-11-02 02:22:15 -0500212 headers = {}
213 headers['X-Auth-Token'] = self.token
214
215 req_url = "%s/%s" % (self.base_url, url)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600216 resp, resp_body = self.http_obj.request(req_url, method,
Zhongyue Luo79d8d362012-09-25 13:49:27 +0800217 headers=headers, body=body)
Attila Fazekas72c7a5f2012-12-03 17:17:23 +0100218
219 #TODO(afazekas): Make sure we can validate all responses, and the
220 #http library does not do any action automatically
221 if (resp.status in set((204, 205, 304)) or resp.status < 200 or
rajalakshmi-ganesana72ee9a2012-12-26 15:03:27 +0530222 method.upper() == 'HEAD') and resp_body:
223 raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
Attila Fazekas72c7a5f2012-12-03 17:17:23 +0100224
225 #NOTE(afazekas):
226 # If the HTTP Status Code is 205
227 # 'The response MUST NOT include an entity.'
228 # A HTTP entity has an entity-body and an 'entity-header'.
229 # In the HTTP response specification (Section 6) the 'entity-header'
230 # 'generic-header' and 'response-header' are in OR relation.
231 # All headers not in the above two group are considered as entity
232 # header in every interpretation.
233
234 if (resp.status == 205 and
235 0 != len(set(resp.keys()) - set(('status',)) -
rajalakshmi-ganesana72ee9a2012-12-26 15:03:27 +0530236 self.response_header_lc - self.general_header_lc)):
237 raise exceptions.ResponseWithEntity()
Attila Fazekas72c7a5f2012-12-03 17:17:23 +0100238
239 #NOTE(afazekas)
240 # Now the swift sometimes (delete not empty container)
241 # returns with non json error response, we can create new rest class
242 # for swift.
243 # Usually RFC2616 says error responses SHOULD contain an explanation.
244 # The warning is normal for SHOULD/SHOULD NOT case
245
246 # Likely it will cause error
247 if not body and resp.status >= 400:
248 self.log.warning("status >= 400 response with empty body")
249
Rohit Karajgi6b1e1542012-05-14 05:55:54 -0700250 if resp.status == 401 or resp.status == 403:
Daryl Walleckced8eb82012-03-19 13:52:37 -0500251 self._log(req_url, body, resp, resp_body)
252 raise exceptions.Unauthorized()
Jay Pipes5135bfc2012-01-05 15:46:49 -0500253
254 if resp.status == 404:
Matthew Treinish426326e2012-11-30 13:17:00 -0500255 if not wait:
256 self._log(req_url, body, resp, resp_body)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600257 raise exceptions.NotFound(resp_body)
Jay Pipes5135bfc2012-01-05 15:46:49 -0500258
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600259 if resp.status == 400:
Dan Smithba6cb162012-08-14 07:22:42 -0700260 resp_body = self._parse_resp(resp_body)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600261 self._log(req_url, body, resp, resp_body)
David Kranz28e35c52012-07-10 10:14:38 -0400262 raise exceptions.BadRequest(resp_body)
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600263
David Kranz5a23d862012-02-14 09:48:55 -0500264 if resp.status == 409:
Dan Smithba6cb162012-08-14 07:22:42 -0700265 resp_body = self._parse_resp(resp_body)
David Kranz5a23d862012-02-14 09:48:55 -0500266 self._log(req_url, body, resp, resp_body)
267 raise exceptions.Duplicate(resp_body)
268
Daryl Wallecked8bef32011-12-05 23:02:08 -0600269 if resp.status == 413:
Dan Smithba6cb162012-08-14 07:22:42 -0700270 resp_body = self._parse_resp(resp_body)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600271 self._log(req_url, body, resp, resp_body)
272 if 'overLimit' in resp_body:
273 raise exceptions.OverLimit(resp_body['overLimit']['message'])
rajalakshmi-ganesana72ee9a2012-12-26 15:03:27 +0530274 elif 'exceeded' in resp_body['message']:
Dan Smithba6cb162012-08-14 07:22:42 -0700275 raise exceptions.OverLimit(resp_body['message'])
Eoghan Glynna5598972012-03-01 09:27:17 -0500276 elif depth < MAX_RECURSION_DEPTH:
277 delay = resp['Retry-After'] if 'Retry-After' in resp else 60
278 time.sleep(int(delay))
279 return self.request(method, url, headers, body, depth + 1)
Jay Pipes9b043842012-01-23 23:34:26 -0500280 else:
281 raise exceptions.RateLimitExceeded(
Daryl Walleck8a707db2012-01-25 00:46:24 -0600282 message=resp_body['overLimitFault']['message'],
283 details=resp_body['overLimitFault']['details'])
Brian Lamar12d9b292011-12-08 12:41:21 -0500284
Daryl Wallecked8bef32011-12-05 23:02:08 -0600285 if resp.status in (500, 501):
Dan Smithba6cb162012-08-14 07:22:42 -0700286 resp_body = self._parse_resp(resp_body)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600287 self._log(req_url, body, resp, resp_body)
Daryl Walleckf0087032011-12-18 13:37:05 -0600288 #I'm seeing both computeFault and cloudServersFault come back.
289 #Will file a bug to fix, but leave as is for now.
290
Daryl Walleck8a707db2012-01-25 00:46:24 -0600291 if 'cloudServersFault' in resp_body:
292 message = resp_body['cloudServersFault']['message']
Jay Pipesedba0622012-07-08 21:34:36 -0400293 elif 'computeFault' in resp_body:
Daryl Walleck8a707db2012-01-25 00:46:24 -0600294 message = resp_body['computeFault']['message']
Jay Pipesedba0622012-07-08 21:34:36 -0400295 elif 'error' in resp_body: # Keystone errors
296 message = resp_body['error']['message']
297 raise exceptions.IdentityError(message)
Dan Princea4b709c2012-10-10 12:27:59 -0400298 elif 'message' in resp_body:
299 message = resp_body['message']
300 else:
301 message = resp_body
302
Daryl Walleckf0087032011-12-18 13:37:05 -0600303 raise exceptions.ComputeFault(message)
Daryl Wallecked8bef32011-12-05 23:02:08 -0600304
David Kranz5a23d862012-02-14 09:48:55 -0500305 if resp.status >= 400:
Dan Smithba6cb162012-08-14 07:22:42 -0700306 resp_body = self._parse_resp(resp_body)
David Kranz5a23d862012-02-14 09:48:55 -0500307 self._log(req_url, body, resp, resp_body)
308 raise exceptions.TempestException(str(resp.status))
309
Daryl Walleck8a707db2012-01-25 00:46:24 -0600310 return resp, resp_body
David Kranz6aceb4a2012-06-05 14:05:45 -0400311
312 def wait_for_resource_deletion(self, id):
Sean Daguef237ccb2013-01-04 15:19:14 -0500313 """Waits for a resource to be deleted."""
David Kranz6aceb4a2012-06-05 14:05:45 -0400314 start_time = int(time.time())
315 while True:
316 if self.is_resource_deleted(id):
317 return
318 if int(time.time()) - start_time >= self.build_timeout:
319 raise exceptions.TimeoutException
320 time.sleep(self.build_interval)
321
322 def is_resource_deleted(self, id):
323 """
324 Subclasses override with specific deletion detection.
325 """
Attila Fazekasd236b4e2013-01-26 00:44:12 +0100326 message = ('"%s" does not implement is_resource_deleted'
327 % self.__class__.__name__)
328 raise NotImplementedError(message)
Dan Smithba6cb162012-08-14 07:22:42 -0700329
330
331class RestClientXML(RestClient):
332 TYPE = "xml"
333
334 def _parse_resp(self, body):
335 return xml_to_json(etree.fromstring(body))