blob: 7beef3fcd2a8eed521e52107d6b80b974bd56417 [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 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
Zhongyue Luo30a563f2012-09-30 23:43:50 +0900119 creds = {
120 'auth': {
Daryl Walleck1465d612011-11-02 02:22:15 -0500121 'passwordCredentials': {
122 'username': user,
Daryl Walleck587385b2012-03-03 13:00:26 -0600123 'password': password,
Daryl Walleck1465d612011-11-02 02:22:15 -0500124 },
Zhongyue Luo30a563f2012-09-30 23:43:50 +0900125 'tenantName': tenant_name,
Daryl Walleck1465d612011-11-02 02:22:15 -0500126 }
127 }
128
Jay Pipescd8eaec2013-01-16 21:03:48 -0500129 dscv = self.config.identity.disable_ssl_certificate_validation
130 self.http_obj = httplib2.Http(disable_ssl_certificate_validation=dscv)
Daryl Walleck1465d612011-11-02 02:22:15 -0500131 headers = {'Content-Type': 'application/json'}
132 body = json.dumps(creds)
133 resp, body = self.http_obj.request(auth_url, 'POST',
134 headers=headers, body=body)
135
Jay Pipes7f757632011-12-02 15:53:32 -0500136 if resp.status == 200:
137 try:
138 auth_data = json.loads(body)['access']
139 token = auth_data['token']['id']
Jay Pipes7f757632011-12-02 15:53:32 -0500140 except Exception, e:
Adam Gandelmane2d46b42012-01-03 17:40:44 -0800141 print "Failed to obtain token for user: %s" % e
Jay Pipes7f757632011-12-02 15:53:32 -0500142 raise
Adam Gandelmane2d46b42012-01-03 17:40:44 -0800143
144 mgmt_url = None
145 for ep in auth_data['serviceCatalog']:
Dan Prince8527c8a2012-12-14 14:00:31 -0500146 if ep["type"] == service:
K Jonathan Harkerd6ba4b42012-12-18 13:50:47 -0800147 for _ep in ep['endpoints']:
148 if service in self.region and \
149 _ep['region'] == self.region[service]:
150 mgmt_url = _ep[self.endpoint_url]
151 if not mgmt_url:
152 mgmt_url = ep['endpoints'][0][self.endpoint_url]
Rohit Karajgid2a28af2012-05-23 03:44:59 -0700153 tenant_id = auth_data['token']['tenant']['id']
Adam Gandelmane2d46b42012-01-03 17:40:44 -0800154 break
155
Zhongyue Luoe471d6e2012-09-17 17:02:43 +0800156 if mgmt_url is None:
Adam Gandelmane2d46b42012-01-03 17:40:44 -0800157 raise exceptions.EndpointNotFound(service)
158
Rohit Karajgid2a28af2012-05-23 03:44:59 -0700159 if service == 'network':
160 # Keystone does not return the correct endpoint for
161 # quantum. Handle this separately.
Zhongyue Luoe0884a32012-09-25 17:24:17 +0800162 mgmt_url = (mgmt_url + self.config.network.api_version +
163 "/tenants/" + tenant_id)
Rohit Karajgid2a28af2012-05-23 03:44:59 -0700164
165 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,
169 password=password)
Daryl Walleck1465d612011-11-02 02:22:15 -0500170
171 def post(self, url, body, headers):
172 return self.request('POST', url, headers, body)
173
Matthew Treinish426326e2012-11-30 13:17:00 -0500174 def get(self, url, headers=None, wait=None):
175 return self.request('GET', url, headers, wait=wait)
Daryl Walleck1465d612011-11-02 02:22:15 -0500176
Dan Smithba6cb162012-08-14 07:22:42 -0700177 def delete(self, url, headers=None):
178 return self.request('DELETE', url, headers)
Daryl Walleck1465d612011-11-02 02:22:15 -0500179
180 def put(self, url, body, headers):
181 return self.request('PUT', url, headers, body)
182
dwalleck5d734432012-10-04 01:11:47 -0500183 def head(self, url, headers=None):
Larisa Ustalov6c3c7802012-11-05 12:25:19 +0200184 return self.request('HEAD', url, headers)
185
186 def copy(self, url, headers=None):
187 return self.request('COPY', url, headers)
dwalleck5d734432012-10-04 01:11:47 -0500188
Daryl Walleck8a707db2012-01-25 00:46:24 -0600189 def _log(self, req_url, body, resp, resp_body):
190 self.log.error('Request URL: ' + req_url)
191 self.log.error('Request Body: ' + str(body))
192 self.log.error('Response Headers: ' + str(resp))
193 self.log.error('Response Body: ' + str(resp_body))
194
Dan Smithba6cb162012-08-14 07:22:42 -0700195 def _parse_resp(self, body):
196 return json.loads(body)
197
Matthew Treinish426326e2012-11-30 13:17:00 -0500198 def request(self, method, url,
199 headers=None, body=None, depth=0, wait=None):
Daryl Wallecke5b83d42011-11-10 14:39:02 -0600200 """A simple HTTP request interface."""
Daryl Walleck1465d612011-11-02 02:22:15 -0500201
chris fattarsi5098fa22012-04-17 13:27:00 -0700202 if (self.token is None) or (self.base_url is None):
203 self._set_auth()
204
Jay Pipescd8eaec2013-01-16 21:03:48 -0500205 dscv = self.config.identity.disable_ssl_certificate_validation
206 self.http_obj = httplib2.Http(disable_ssl_certificate_validation=dscv)
Zhongyue Luoe471d6e2012-09-17 17:02:43 +0800207 if headers is None:
Daryl Walleck1465d612011-11-02 02:22:15 -0500208 headers = {}
209 headers['X-Auth-Token'] = self.token
210
211 req_url = "%s/%s" % (self.base_url, url)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600212 resp, resp_body = self.http_obj.request(req_url, method,
Zhongyue Luo79d8d362012-09-25 13:49:27 +0800213 headers=headers, body=body)
Attila Fazekas72c7a5f2012-12-03 17:17:23 +0100214
215 #TODO(afazekas): Make sure we can validate all responses, and the
216 #http library does not do any action automatically
217 if (resp.status in set((204, 205, 304)) or resp.status < 200 or
rajalakshmi-ganesana72ee9a2012-12-26 15:03:27 +0530218 method.upper() == 'HEAD') and resp_body:
219 raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
Attila Fazekas72c7a5f2012-12-03 17:17:23 +0100220
221 #NOTE(afazekas):
222 # If the HTTP Status Code is 205
223 # 'The response MUST NOT include an entity.'
224 # A HTTP entity has an entity-body and an 'entity-header'.
225 # In the HTTP response specification (Section 6) the 'entity-header'
226 # 'generic-header' and 'response-header' are in OR relation.
227 # All headers not in the above two group are considered as entity
228 # header in every interpretation.
229
230 if (resp.status == 205 and
231 0 != len(set(resp.keys()) - set(('status',)) -
rajalakshmi-ganesana72ee9a2012-12-26 15:03:27 +0530232 self.response_header_lc - self.general_header_lc)):
233 raise exceptions.ResponseWithEntity()
Attila Fazekas72c7a5f2012-12-03 17:17:23 +0100234
235 #NOTE(afazekas)
236 # Now the swift sometimes (delete not empty container)
237 # returns with non json error response, we can create new rest class
238 # for swift.
239 # Usually RFC2616 says error responses SHOULD contain an explanation.
240 # The warning is normal for SHOULD/SHOULD NOT case
241
242 # Likely it will cause error
243 if not body and resp.status >= 400:
244 self.log.warning("status >= 400 response with empty body")
245
Rohit Karajgi6b1e1542012-05-14 05:55:54 -0700246 if resp.status == 401 or resp.status == 403:
Daryl Walleckced8eb82012-03-19 13:52:37 -0500247 self._log(req_url, body, resp, resp_body)
248 raise exceptions.Unauthorized()
Jay Pipes5135bfc2012-01-05 15:46:49 -0500249
250 if resp.status == 404:
Matthew Treinish426326e2012-11-30 13:17:00 -0500251 if not wait:
252 self._log(req_url, body, resp, resp_body)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600253 raise exceptions.NotFound(resp_body)
Jay Pipes5135bfc2012-01-05 15:46:49 -0500254
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600255 if resp.status == 400:
Dan Smithba6cb162012-08-14 07:22:42 -0700256 resp_body = self._parse_resp(resp_body)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600257 self._log(req_url, body, resp, resp_body)
David Kranz28e35c52012-07-10 10:14:38 -0400258 raise exceptions.BadRequest(resp_body)
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600259
David Kranz5a23d862012-02-14 09:48:55 -0500260 if resp.status == 409:
Dan Smithba6cb162012-08-14 07:22:42 -0700261 resp_body = self._parse_resp(resp_body)
David Kranz5a23d862012-02-14 09:48:55 -0500262 self._log(req_url, body, resp, resp_body)
263 raise exceptions.Duplicate(resp_body)
264
Daryl Wallecked8bef32011-12-05 23:02:08 -0600265 if resp.status == 413:
Dan Smithba6cb162012-08-14 07:22:42 -0700266 resp_body = self._parse_resp(resp_body)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600267 self._log(req_url, body, resp, resp_body)
268 if 'overLimit' in resp_body:
269 raise exceptions.OverLimit(resp_body['overLimit']['message'])
rajalakshmi-ganesana72ee9a2012-12-26 15:03:27 +0530270 elif 'exceeded' in resp_body['message']:
Dan Smithba6cb162012-08-14 07:22:42 -0700271 raise exceptions.OverLimit(resp_body['message'])
Eoghan Glynna5598972012-03-01 09:27:17 -0500272 elif depth < MAX_RECURSION_DEPTH:
273 delay = resp['Retry-After'] if 'Retry-After' in resp else 60
274 time.sleep(int(delay))
275 return self.request(method, url, headers, body, depth + 1)
Jay Pipes9b043842012-01-23 23:34:26 -0500276 else:
277 raise exceptions.RateLimitExceeded(
Daryl Walleck8a707db2012-01-25 00:46:24 -0600278 message=resp_body['overLimitFault']['message'],
279 details=resp_body['overLimitFault']['details'])
Brian Lamar12d9b292011-12-08 12:41:21 -0500280
Daryl Wallecked8bef32011-12-05 23:02:08 -0600281 if resp.status in (500, 501):
Dan Smithba6cb162012-08-14 07:22:42 -0700282 resp_body = self._parse_resp(resp_body)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600283 self._log(req_url, body, resp, resp_body)
Daryl Walleckf0087032011-12-18 13:37:05 -0600284 #I'm seeing both computeFault and cloudServersFault come back.
285 #Will file a bug to fix, but leave as is for now.
286
Daryl Walleck8a707db2012-01-25 00:46:24 -0600287 if 'cloudServersFault' in resp_body:
288 message = resp_body['cloudServersFault']['message']
Jay Pipesedba0622012-07-08 21:34:36 -0400289 elif 'computeFault' in resp_body:
Daryl Walleck8a707db2012-01-25 00:46:24 -0600290 message = resp_body['computeFault']['message']
Jay Pipesedba0622012-07-08 21:34:36 -0400291 elif 'error' in resp_body: # Keystone errors
292 message = resp_body['error']['message']
293 raise exceptions.IdentityError(message)
Dan Princea4b709c2012-10-10 12:27:59 -0400294 elif 'message' in resp_body:
295 message = resp_body['message']
296 else:
297 message = resp_body
298
Daryl Walleckf0087032011-12-18 13:37:05 -0600299 raise exceptions.ComputeFault(message)
Daryl Wallecked8bef32011-12-05 23:02:08 -0600300
David Kranz5a23d862012-02-14 09:48:55 -0500301 if resp.status >= 400:
Dan Smithba6cb162012-08-14 07:22:42 -0700302 resp_body = self._parse_resp(resp_body)
David Kranz5a23d862012-02-14 09:48:55 -0500303 self._log(req_url, body, resp, resp_body)
304 raise exceptions.TempestException(str(resp.status))
305
Daryl Walleck8a707db2012-01-25 00:46:24 -0600306 return resp, resp_body
David Kranz6aceb4a2012-06-05 14:05:45 -0400307
308 def wait_for_resource_deletion(self, id):
Sean Daguef237ccb2013-01-04 15:19:14 -0500309 """Waits for a resource to be deleted."""
David Kranz6aceb4a2012-06-05 14:05:45 -0400310 start_time = int(time.time())
311 while True:
312 if self.is_resource_deleted(id):
313 return
314 if int(time.time()) - start_time >= self.build_timeout:
315 raise exceptions.TimeoutException
316 time.sleep(self.build_interval)
317
318 def is_resource_deleted(self, id):
319 """
320 Subclasses override with specific deletion detection.
321 """
322 return False
Dan Smithba6cb162012-08-14 07:22:42 -0700323
324
325class RestClientXML(RestClient):
326 TYPE = "xml"
327
328 def _parse_resp(self, body):
329 return xml_to_json(etree.fromstring(body))