blob: cb18a9cdeb48b73f1c0af9f3d414aabfc8f6ff7f [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
47 self.region = 0
48 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:
chris fattarsi5098fa22012-04-17 13:27:00 -0700145 mgmt_url = ep['endpoints'][self.region][self.endpoint_url]
Rohit Karajgid2a28af2012-05-23 03:44:59 -0700146 tenant_id = auth_data['token']['tenant']['id']
Adam Gandelmane2d46b42012-01-03 17:40:44 -0800147 break
148
Zhongyue Luoe471d6e2012-09-17 17:02:43 +0800149 if mgmt_url is None:
Adam Gandelmane2d46b42012-01-03 17:40:44 -0800150 raise exceptions.EndpointNotFound(service)
151
Rohit Karajgid2a28af2012-05-23 03:44:59 -0700152 if service == 'network':
153 # Keystone does not return the correct endpoint for
154 # quantum. Handle this separately.
Zhongyue Luoe0884a32012-09-25 17:24:17 +0800155 mgmt_url = (mgmt_url + self.config.network.api_version +
156 "/tenants/" + tenant_id)
Rohit Karajgid2a28af2012-05-23 03:44:59 -0700157
158 return token, mgmt_url
159
Jay Pipes7f757632011-12-02 15:53:32 -0500160 elif resp.status == 401:
Daryl Wallecka22f57b2012-03-20 16:52:07 -0500161 raise exceptions.AuthenticationFailure(user=user,
162 password=password)
Daryl Walleck1465d612011-11-02 02:22:15 -0500163
164 def post(self, url, body, headers):
165 return self.request('POST', url, headers, body)
166
Matthew Treinish426326e2012-11-30 13:17:00 -0500167 def get(self, url, headers=None, wait=None):
168 return self.request('GET', url, headers, wait=wait)
Daryl Walleck1465d612011-11-02 02:22:15 -0500169
Dan Smithba6cb162012-08-14 07:22:42 -0700170 def delete(self, url, headers=None):
171 return self.request('DELETE', url, headers)
Daryl Walleck1465d612011-11-02 02:22:15 -0500172
173 def put(self, url, body, headers):
174 return self.request('PUT', url, headers, body)
175
dwalleck5d734432012-10-04 01:11:47 -0500176 def head(self, url, headers=None):
Larisa Ustalov6c3c7802012-11-05 12:25:19 +0200177 return self.request('HEAD', url, headers)
178
179 def copy(self, url, headers=None):
180 return self.request('COPY', url, headers)
dwalleck5d734432012-10-04 01:11:47 -0500181
Daryl Walleck8a707db2012-01-25 00:46:24 -0600182 def _log(self, req_url, body, resp, resp_body):
183 self.log.error('Request URL: ' + req_url)
184 self.log.error('Request Body: ' + str(body))
185 self.log.error('Response Headers: ' + str(resp))
186 self.log.error('Response Body: ' + str(resp_body))
187
Dan Smithba6cb162012-08-14 07:22:42 -0700188 def _parse_resp(self, body):
189 return json.loads(body)
190
Matthew Treinish426326e2012-11-30 13:17:00 -0500191 def request(self, method, url,
192 headers=None, body=None, depth=0, wait=None):
Daryl Wallecke5b83d42011-11-10 14:39:02 -0600193 """A simple HTTP request interface."""
Daryl Walleck1465d612011-11-02 02:22:15 -0500194
chris fattarsi5098fa22012-04-17 13:27:00 -0700195 if (self.token is None) or (self.base_url is None):
196 self._set_auth()
197
Jay Pipese9e24dd2012-12-13 00:09:34 -0500198 self.http_obj = httplib2.Http(disable_ssl_certificate_validation=True)
Zhongyue Luoe471d6e2012-09-17 17:02:43 +0800199 if headers is None:
Daryl Walleck1465d612011-11-02 02:22:15 -0500200 headers = {}
201 headers['X-Auth-Token'] = self.token
202
203 req_url = "%s/%s" % (self.base_url, url)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600204 resp, resp_body = self.http_obj.request(req_url, method,
Zhongyue Luo79d8d362012-09-25 13:49:27 +0800205 headers=headers, body=body)
Attila Fazekas72c7a5f2012-12-03 17:17:23 +0100206
207 #TODO(afazekas): Make sure we can validate all responses, and the
208 #http library does not do any action automatically
209 if (resp.status in set((204, 205, 304)) or resp.status < 200 or
Armando Migliaccio49494392012-12-12 18:53:30 +0000210 method.upper() == 'HEAD') and resp_body:
Armando Migliaccio41de64f2012-12-12 13:44:34 +0000211 raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
Attila Fazekas72c7a5f2012-12-03 17:17:23 +0100212
213 #NOTE(afazekas):
214 # If the HTTP Status Code is 205
215 # 'The response MUST NOT include an entity.'
216 # A HTTP entity has an entity-body and an 'entity-header'.
217 # In the HTTP response specification (Section 6) the 'entity-header'
218 # 'generic-header' and 'response-header' are in OR relation.
219 # All headers not in the above two group are considered as entity
220 # header in every interpretation.
221
222 if (resp.status == 205 and
223 0 != len(set(resp.keys()) - set(('status',)) -
224 self.response_header_lc - self.general_header_lc)):
Armando Migliaccio41de64f2012-12-12 13:44:34 +0000225 raise exceptions.ResponseWithEntity()
Attila Fazekas72c7a5f2012-12-03 17:17:23 +0100226
227 #NOTE(afazekas)
228 # Now the swift sometimes (delete not empty container)
229 # returns with non json error response, we can create new rest class
230 # for swift.
231 # Usually RFC2616 says error responses SHOULD contain an explanation.
232 # The warning is normal for SHOULD/SHOULD NOT case
233
234 # Likely it will cause error
235 if not body and resp.status >= 400:
236 self.log.warning("status >= 400 response with empty body")
237
Rohit Karajgi6b1e1542012-05-14 05:55:54 -0700238 if resp.status == 401 or resp.status == 403:
Daryl Walleckced8eb82012-03-19 13:52:37 -0500239 self._log(req_url, body, resp, resp_body)
240 raise exceptions.Unauthorized()
Jay Pipes5135bfc2012-01-05 15:46:49 -0500241
242 if resp.status == 404:
Matthew Treinish426326e2012-11-30 13:17:00 -0500243 if not wait:
244 self._log(req_url, body, resp, resp_body)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600245 raise exceptions.NotFound(resp_body)
Jay Pipes5135bfc2012-01-05 15:46:49 -0500246
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600247 if resp.status == 400:
Dan Smithba6cb162012-08-14 07:22:42 -0700248 resp_body = self._parse_resp(resp_body)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600249 self._log(req_url, body, resp, resp_body)
David Kranz28e35c52012-07-10 10:14:38 -0400250 raise exceptions.BadRequest(resp_body)
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600251
David Kranz5a23d862012-02-14 09:48:55 -0500252 if resp.status == 409:
Dan Smithba6cb162012-08-14 07:22:42 -0700253 resp_body = self._parse_resp(resp_body)
David Kranz5a23d862012-02-14 09:48:55 -0500254 self._log(req_url, body, resp, resp_body)
255 raise exceptions.Duplicate(resp_body)
256
Daryl Wallecked8bef32011-12-05 23:02:08 -0600257 if resp.status == 413:
Dan Smithba6cb162012-08-14 07:22:42 -0700258 resp_body = self._parse_resp(resp_body)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600259 self._log(req_url, body, resp, resp_body)
260 if 'overLimit' in resp_body:
261 raise exceptions.OverLimit(resp_body['overLimit']['message'])
Dan Smithba6cb162012-08-14 07:22:42 -0700262 elif 'limit' in resp_body['message']:
263 raise exceptions.OverLimit(resp_body['message'])
Eoghan Glynna5598972012-03-01 09:27:17 -0500264 elif depth < MAX_RECURSION_DEPTH:
265 delay = resp['Retry-After'] if 'Retry-After' in resp else 60
266 time.sleep(int(delay))
267 return self.request(method, url, headers, body, depth + 1)
Jay Pipes9b043842012-01-23 23:34:26 -0500268 else:
269 raise exceptions.RateLimitExceeded(
Daryl Walleck8a707db2012-01-25 00:46:24 -0600270 message=resp_body['overLimitFault']['message'],
271 details=resp_body['overLimitFault']['details'])
Brian Lamar12d9b292011-12-08 12:41:21 -0500272
Daryl Wallecked8bef32011-12-05 23:02:08 -0600273 if resp.status in (500, 501):
Dan Smithba6cb162012-08-14 07:22:42 -0700274 resp_body = self._parse_resp(resp_body)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600275 self._log(req_url, body, resp, resp_body)
Daryl Walleckf0087032011-12-18 13:37:05 -0600276 #I'm seeing both computeFault and cloudServersFault come back.
277 #Will file a bug to fix, but leave as is for now.
278
Daryl Walleck8a707db2012-01-25 00:46:24 -0600279 if 'cloudServersFault' in resp_body:
280 message = resp_body['cloudServersFault']['message']
Jay Pipesedba0622012-07-08 21:34:36 -0400281 elif 'computeFault' in resp_body:
Daryl Walleck8a707db2012-01-25 00:46:24 -0600282 message = resp_body['computeFault']['message']
Jay Pipesedba0622012-07-08 21:34:36 -0400283 elif 'error' in resp_body: # Keystone errors
284 message = resp_body['error']['message']
285 raise exceptions.IdentityError(message)
Dan Princea4b709c2012-10-10 12:27:59 -0400286 elif 'message' in resp_body:
287 message = resp_body['message']
288 else:
289 message = resp_body
290
Daryl Walleckf0087032011-12-18 13:37:05 -0600291 raise exceptions.ComputeFault(message)
Daryl Wallecked8bef32011-12-05 23:02:08 -0600292
David Kranz5a23d862012-02-14 09:48:55 -0500293 if resp.status >= 400:
Dan Smithba6cb162012-08-14 07:22:42 -0700294 resp_body = self._parse_resp(resp_body)
David Kranz5a23d862012-02-14 09:48:55 -0500295 self._log(req_url, body, resp, resp_body)
296 raise exceptions.TempestException(str(resp.status))
297
Daryl Walleck8a707db2012-01-25 00:46:24 -0600298 return resp, resp_body
David Kranz6aceb4a2012-06-05 14:05:45 -0400299
300 def wait_for_resource_deletion(self, id):
301 """Waits for a resource to be deleted"""
302 start_time = int(time.time())
303 while True:
304 if self.is_resource_deleted(id):
305 return
306 if int(time.time()) - start_time >= self.build_timeout:
307 raise exceptions.TimeoutException
308 time.sleep(self.build_interval)
309
310 def is_resource_deleted(self, id):
311 """
312 Subclasses override with specific deletion detection.
313 """
314 return False
Dan Smithba6cb162012-08-14 07:22:42 -0700315
316
317class RestClientXML(RestClient):
318 TYPE = "xml"
319
320 def _parse_resp(self, body):
321 return xml_to_json(etree.fromstring(body))