blob: 884d147a0590136f7bfb0f97d7a0505074048463 [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
K Jonathan Harkerd6ba4b42012-12-18 13:50:47 -080047 self.region = {'compute': self.config.compute.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 Pipese9e24dd2012-12-13 00:09:34 -0500106 self.http_obj = httplib2.Http(disable_ssl_certificate_validation=True)
Daryl Walleck1465d612011-11-02 02:22:15 -0500107 resp, body = self.http_obj.request(auth_url, 'GET', **params)
108 try:
109 return resp['x-auth-token'], resp['x-server-management-url']
Matthew Treinish05d9fb92012-12-07 16:14:05 -0500110 except Exception:
Daryl Walleck1465d612011-11-02 02:22:15 -0500111 raise
112
Daryl Walleck587385b2012-03-03 13:00:26 -0600113 def keystone_auth(self, user, password, auth_url, service, tenant_name):
Daryl Walleck1465d612011-11-02 02:22:15 -0500114 """
Daryl Walleck587385b2012-03-03 13:00:26 -0600115 Provides authentication via Keystone
Daryl Walleck1465d612011-11-02 02:22:15 -0500116 """
117
Zhongyue Luo30a563f2012-09-30 23:43:50 +0900118 creds = {
119 'auth': {
Daryl Walleck1465d612011-11-02 02:22:15 -0500120 'passwordCredentials': {
121 'username': user,
Daryl Walleck587385b2012-03-03 13:00:26 -0600122 'password': password,
Daryl Walleck1465d612011-11-02 02:22:15 -0500123 },
Zhongyue Luo30a563f2012-09-30 23:43:50 +0900124 'tenantName': tenant_name,
Daryl Walleck1465d612011-11-02 02:22:15 -0500125 }
126 }
127
Jay Pipese9e24dd2012-12-13 00:09:34 -0500128 self.http_obj = httplib2.Http(disable_ssl_certificate_validation=True)
Daryl Walleck1465d612011-11-02 02:22:15 -0500129 headers = {'Content-Type': 'application/json'}
130 body = json.dumps(creds)
131 resp, body = self.http_obj.request(auth_url, 'POST',
132 headers=headers, body=body)
133
Jay Pipes7f757632011-12-02 15:53:32 -0500134 if resp.status == 200:
135 try:
136 auth_data = json.loads(body)['access']
137 token = auth_data['token']['id']
Jay Pipes7f757632011-12-02 15:53:32 -0500138 except Exception, e:
Adam Gandelmane2d46b42012-01-03 17:40:44 -0800139 print "Failed to obtain token for user: %s" % e
Jay Pipes7f757632011-12-02 15:53:32 -0500140 raise
Adam Gandelmane2d46b42012-01-03 17:40:44 -0800141
142 mgmt_url = None
143 for ep in auth_data['serviceCatalog']:
Dan Prince8527c8a2012-12-14 14:00:31 -0500144 if ep["type"] == service:
K Jonathan Harkerd6ba4b42012-12-18 13:50:47 -0800145 for _ep in ep['endpoints']:
146 if service in self.region and \
147 _ep['region'] == self.region[service]:
148 mgmt_url = _ep[self.endpoint_url]
149 if not mgmt_url:
150 mgmt_url = ep['endpoints'][0][self.endpoint_url]
Rohit Karajgid2a28af2012-05-23 03:44:59 -0700151 tenant_id = auth_data['token']['tenant']['id']
Adam Gandelmane2d46b42012-01-03 17:40:44 -0800152 break
153
Zhongyue Luoe471d6e2012-09-17 17:02:43 +0800154 if mgmt_url is None:
Adam Gandelmane2d46b42012-01-03 17:40:44 -0800155 raise exceptions.EndpointNotFound(service)
156
Rohit Karajgid2a28af2012-05-23 03:44:59 -0700157 if service == 'network':
158 # Keystone does not return the correct endpoint for
159 # quantum. Handle this separately.
Zhongyue Luoe0884a32012-09-25 17:24:17 +0800160 mgmt_url = (mgmt_url + self.config.network.api_version +
161 "/tenants/" + tenant_id)
Rohit Karajgid2a28af2012-05-23 03:44:59 -0700162
163 return token, mgmt_url
164
Jay Pipes7f757632011-12-02 15:53:32 -0500165 elif resp.status == 401:
Daryl Wallecka22f57b2012-03-20 16:52:07 -0500166 raise exceptions.AuthenticationFailure(user=user,
167 password=password)
Daryl Walleck1465d612011-11-02 02:22:15 -0500168
169 def post(self, url, body, headers):
170 return self.request('POST', url, headers, body)
171
Matthew Treinish426326e2012-11-30 13:17:00 -0500172 def get(self, url, headers=None, wait=None):
173 return self.request('GET', url, headers, wait=wait)
Daryl Walleck1465d612011-11-02 02:22:15 -0500174
Dan Smithba6cb162012-08-14 07:22:42 -0700175 def delete(self, url, headers=None):
176 return self.request('DELETE', url, headers)
Daryl Walleck1465d612011-11-02 02:22:15 -0500177
178 def put(self, url, body, headers):
179 return self.request('PUT', url, headers, body)
180
dwalleck5d734432012-10-04 01:11:47 -0500181 def head(self, url, headers=None):
Larisa Ustalov6c3c7802012-11-05 12:25:19 +0200182 return self.request('HEAD', url, headers)
183
184 def copy(self, url, headers=None):
185 return self.request('COPY', url, headers)
dwalleck5d734432012-10-04 01:11:47 -0500186
Daryl Walleck8a707db2012-01-25 00:46:24 -0600187 def _log(self, req_url, body, resp, resp_body):
188 self.log.error('Request URL: ' + req_url)
189 self.log.error('Request Body: ' + str(body))
190 self.log.error('Response Headers: ' + str(resp))
191 self.log.error('Response Body: ' + str(resp_body))
192
Dan Smithba6cb162012-08-14 07:22:42 -0700193 def _parse_resp(self, body):
194 return json.loads(body)
195
Matthew Treinish426326e2012-11-30 13:17:00 -0500196 def request(self, method, url,
197 headers=None, body=None, depth=0, wait=None):
Daryl Wallecke5b83d42011-11-10 14:39:02 -0600198 """A simple HTTP request interface."""
Daryl Walleck1465d612011-11-02 02:22:15 -0500199
chris fattarsi5098fa22012-04-17 13:27:00 -0700200 if (self.token is None) or (self.base_url is None):
201 self._set_auth()
202
Jay Pipese9e24dd2012-12-13 00:09:34 -0500203 self.http_obj = httplib2.Http(disable_ssl_certificate_validation=True)
Zhongyue Luoe471d6e2012-09-17 17:02:43 +0800204 if headers is None:
Daryl Walleck1465d612011-11-02 02:22:15 -0500205 headers = {}
206 headers['X-Auth-Token'] = self.token
207
208 req_url = "%s/%s" % (self.base_url, url)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600209 resp, resp_body = self.http_obj.request(req_url, method,
Zhongyue Luo79d8d362012-09-25 13:49:27 +0800210 headers=headers, body=body)
Attila Fazekas72c7a5f2012-12-03 17:17:23 +0100211
212 #TODO(afazekas): Make sure we can validate all responses, and the
213 #http library does not do any action automatically
214 if (resp.status in set((204, 205, 304)) or resp.status < 200 or
Armando Migliaccio49494392012-12-12 18:53:30 +0000215 method.upper() == 'HEAD') and resp_body:
Armando Migliaccio41de64f2012-12-12 13:44:34 +0000216 raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
Attila Fazekas72c7a5f2012-12-03 17:17:23 +0100217
218 #NOTE(afazekas):
219 # If the HTTP Status Code is 205
220 # 'The response MUST NOT include an entity.'
221 # A HTTP entity has an entity-body and an 'entity-header'.
222 # In the HTTP response specification (Section 6) the 'entity-header'
223 # 'generic-header' and 'response-header' are in OR relation.
224 # All headers not in the above two group are considered as entity
225 # header in every interpretation.
226
227 if (resp.status == 205 and
228 0 != len(set(resp.keys()) - set(('status',)) -
229 self.response_header_lc - self.general_header_lc)):
Armando Migliaccio41de64f2012-12-12 13:44:34 +0000230 raise exceptions.ResponseWithEntity()
Attila Fazekas72c7a5f2012-12-03 17:17:23 +0100231
232 #NOTE(afazekas)
233 # Now the swift sometimes (delete not empty container)
234 # returns with non json error response, we can create new rest class
235 # for swift.
236 # Usually RFC2616 says error responses SHOULD contain an explanation.
237 # The warning is normal for SHOULD/SHOULD NOT case
238
239 # Likely it will cause error
240 if not body and resp.status >= 400:
241 self.log.warning("status >= 400 response with empty body")
242
Rohit Karajgi6b1e1542012-05-14 05:55:54 -0700243 if resp.status == 401 or resp.status == 403:
Daryl Walleckced8eb82012-03-19 13:52:37 -0500244 self._log(req_url, body, resp, resp_body)
245 raise exceptions.Unauthorized()
Jay Pipes5135bfc2012-01-05 15:46:49 -0500246
247 if resp.status == 404:
Matthew Treinish426326e2012-11-30 13:17:00 -0500248 if not wait:
249 self._log(req_url, body, resp, resp_body)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600250 raise exceptions.NotFound(resp_body)
Jay Pipes5135bfc2012-01-05 15:46:49 -0500251
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600252 if resp.status == 400:
Dan Smithba6cb162012-08-14 07:22:42 -0700253 resp_body = self._parse_resp(resp_body)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600254 self._log(req_url, body, resp, resp_body)
David Kranz28e35c52012-07-10 10:14:38 -0400255 raise exceptions.BadRequest(resp_body)
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600256
David Kranz5a23d862012-02-14 09:48:55 -0500257 if resp.status == 409:
Dan Smithba6cb162012-08-14 07:22:42 -0700258 resp_body = self._parse_resp(resp_body)
David Kranz5a23d862012-02-14 09:48:55 -0500259 self._log(req_url, body, resp, resp_body)
260 raise exceptions.Duplicate(resp_body)
261
Daryl Wallecked8bef32011-12-05 23:02:08 -0600262 if resp.status == 413:
Dan Smithba6cb162012-08-14 07:22:42 -0700263 resp_body = self._parse_resp(resp_body)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600264 self._log(req_url, body, resp, resp_body)
265 if 'overLimit' in resp_body:
266 raise exceptions.OverLimit(resp_body['overLimit']['message'])
Dan Smithba6cb162012-08-14 07:22:42 -0700267 elif 'limit' in resp_body['message']:
268 raise exceptions.OverLimit(resp_body['message'])
Eoghan Glynna5598972012-03-01 09:27:17 -0500269 elif depth < MAX_RECURSION_DEPTH:
270 delay = resp['Retry-After'] if 'Retry-After' in resp else 60
271 time.sleep(int(delay))
272 return self.request(method, url, headers, body, depth + 1)
Jay Pipes9b043842012-01-23 23:34:26 -0500273 else:
274 raise exceptions.RateLimitExceeded(
Daryl Walleck8a707db2012-01-25 00:46:24 -0600275 message=resp_body['overLimitFault']['message'],
276 details=resp_body['overLimitFault']['details'])
Brian Lamar12d9b292011-12-08 12:41:21 -0500277
Daryl Wallecked8bef32011-12-05 23:02:08 -0600278 if resp.status in (500, 501):
Dan Smithba6cb162012-08-14 07:22:42 -0700279 resp_body = self._parse_resp(resp_body)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600280 self._log(req_url, body, resp, resp_body)
Daryl Walleckf0087032011-12-18 13:37:05 -0600281 #I'm seeing both computeFault and cloudServersFault come back.
282 #Will file a bug to fix, but leave as is for now.
283
Daryl Walleck8a707db2012-01-25 00:46:24 -0600284 if 'cloudServersFault' in resp_body:
285 message = resp_body['cloudServersFault']['message']
Jay Pipesedba0622012-07-08 21:34:36 -0400286 elif 'computeFault' in resp_body:
Daryl Walleck8a707db2012-01-25 00:46:24 -0600287 message = resp_body['computeFault']['message']
Jay Pipesedba0622012-07-08 21:34:36 -0400288 elif 'error' in resp_body: # Keystone errors
289 message = resp_body['error']['message']
290 raise exceptions.IdentityError(message)
Dan Princea4b709c2012-10-10 12:27:59 -0400291 elif 'message' in resp_body:
292 message = resp_body['message']
293 else:
294 message = resp_body
295
Daryl Walleckf0087032011-12-18 13:37:05 -0600296 raise exceptions.ComputeFault(message)
Daryl Wallecked8bef32011-12-05 23:02:08 -0600297
David Kranz5a23d862012-02-14 09:48:55 -0500298 if resp.status >= 400:
Dan Smithba6cb162012-08-14 07:22:42 -0700299 resp_body = self._parse_resp(resp_body)
David Kranz5a23d862012-02-14 09:48:55 -0500300 self._log(req_url, body, resp, resp_body)
301 raise exceptions.TempestException(str(resp.status))
302
Daryl Walleck8a707db2012-01-25 00:46:24 -0600303 return resp, resp_body
David Kranz6aceb4a2012-06-05 14:05:45 -0400304
305 def wait_for_resource_deletion(self, id):
306 """Waits for a resource to be deleted"""
307 start_time = int(time.time())
308 while True:
309 if self.is_resource_deleted(id):
310 return
311 if int(time.time()) - start_time >= self.build_timeout:
312 raise exceptions.TimeoutException
313 time.sleep(self.build_interval)
314
315 def is_resource_deleted(self, id):
316 """
317 Subclasses override with specific deletion detection.
318 """
319 return False
Dan Smithba6cb162012-08-14 07:22:42 -0700320
321
322class RestClientXML(RestClient):
323 TYPE = "xml"
324
325 def _parse_resp(self, body):
326 return xml_to_json(etree.fromstring(body))