blob: 8a85602bba75716489e44b7bb6c419461dc4636a [file] [log] [blame]
ZhiQiang Fan39f97222013-09-20 04:49:44 +08001# Copyright 2012 OpenStack Foundation
Brant Knudsonc7ca3342013-03-28 21:08:50 -05002# Copyright 2013 IBM Corp.
Jay Pipes3f981df2012-03-27 18:59:44 -04003# All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
Attila Fazekas55f6d8c2013-03-10 10:32:54 +010017import collections
Attila Fazekas11d2a772013-01-29 17:46:52 +010018import hashlib
Matthew Treinisha83a16e2012-12-07 13:44:02 -050019import json
Dan Smithba6cb162012-08-14 07:22:42 -070020from lxml import etree
Attila Fazekas11d2a772013-01-29 17:46:52 +010021import re
Eoghan Glynna5598972012-03-01 09:27:17 -050022import time
Jay Pipes3f981df2012-03-27 18:59:44 -040023
Mate Lakat23a58a32013-08-23 02:06:22 +010024from tempest.common import http
Matthew Treinish684d8992014-01-30 16:27:40 +000025from tempest import config
Daryl Wallecked8bef32011-12-05 23:02:08 -060026from tempest import exceptions
Matthew Treinishf4a9b0f2013-07-26 16:58:26 -040027from tempest.openstack.common import log as logging
dwallecke62b9f02012-10-10 23:34:42 -050028from tempest.services.compute.xml.common import xml_to_json
Daryl Walleck1465d612011-11-02 02:22:15 -050029
Matthew Treinish684d8992014-01-30 16:27:40 +000030CONF = config.CONF
31
Eoghan Glynna5598972012-03-01 09:27:17 -050032# redrive rate limited calls at most twice
33MAX_RECURSION_DEPTH = 2
Attila Fazekas11d2a772013-01-29 17:46:52 +010034TOKEN_CHARS_RE = re.compile('^[-A-Za-z0-9+/=]*$')
Eoghan Glynna5598972012-03-01 09:27:17 -050035
Attila Fazekas54a42862013-07-28 22:31:06 +020036# All the successful HTTP status codes from RFC 2616
37HTTP_SUCCESS = (200, 201, 202, 203, 204, 205, 206)
38
Eoghan Glynna5598972012-03-01 09:27:17 -050039
Daryl Walleck1465d612011-11-02 02:22:15 -050040class RestClient(object):
Dan Smithba6cb162012-08-14 07:22:42 -070041 TYPE = "json"
Attila Fazekas11d2a772013-01-29 17:46:52 +010042 LOG = logging.getLogger(__name__)
Daryl Walleck1465d612011-11-02 02:22:15 -050043
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000044 def __init__(self, auth_provider):
45 self.auth_provider = auth_provider
chris fattarsi5098fa22012-04-17 13:27:00 -070046
chris fattarsi5098fa22012-04-17 13:27:00 -070047 self.endpoint_url = 'publicURL'
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000048 self.service = None
49 # The version of the API this client implements
50 self.api_version = None
51 self._skip_path = False
Dan Smithba6cb162012-08-14 07:22:42 -070052 self.headers = {'Content-Type': 'application/%s' % self.TYPE,
53 'Accept': 'application/%s' % self.TYPE}
Matthew Treinish684d8992014-01-30 16:27:40 +000054 self.build_interval = CONF.compute.build_interval
55 self.build_timeout = CONF.compute.build_timeout
Attila Fazekas72c7a5f2012-12-03 17:17:23 +010056 self.general_header_lc = set(('cache-control', 'connection',
57 'date', 'pragma', 'trailer',
58 'transfer-encoding', 'via',
59 'warning'))
60 self.response_header_lc = set(('accept-ranges', 'age', 'etag',
61 'location', 'proxy-authenticate',
62 'retry-after', 'server',
63 'vary', 'www-authenticate'))
Matthew Treinish684d8992014-01-30 16:27:40 +000064 dscv = CONF.identity.disable_ssl_certificate_validation
Mate Lakat23a58a32013-08-23 02:06:22 +010065 self.http_obj = http.ClosingHttp(
66 disable_ssl_certificate_validation=dscv)
chris fattarsi5098fa22012-04-17 13:27:00 -070067
DennyZhang7be75002013-09-19 06:55:11 -050068 def __str__(self):
69 STRING_LIMIT = 80
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000070 str_format = ("config:%s, service:%s, base_url:%s, "
71 "filters: %s, build_interval:%s, build_timeout:%s"
DennyZhang7be75002013-09-19 06:55:11 -050072 "\ntoken:%s..., \nheaders:%s...")
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000073 return str_format % (CONF, self.service, self.base_url,
74 self.filters, self.build_interval,
75 self.build_timeout,
DennyZhang7be75002013-09-19 06:55:11 -050076 str(self.token)[0:STRING_LIMIT],
77 str(self.headers)[0:STRING_LIMIT])
78
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000079 def _get_region(self, service):
chris fattarsi5098fa22012-04-17 13:27:00 -070080 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000081 Returns the region for a specific service
chris fattarsi5098fa22012-04-17 13:27:00 -070082 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000083 service_region = None
84 for cfgname in dir(CONF._config):
85 # Find all config.FOO.catalog_type and assume FOO is a service.
86 cfg = getattr(CONF, cfgname)
87 catalog_type = getattr(cfg, 'catalog_type', None)
88 if catalog_type == service:
89 service_region = getattr(cfg, 'region', None)
90 if not service_region:
91 service_region = CONF.identity.region
92 return service_region
chris fattarsi5098fa22012-04-17 13:27:00 -070093
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000094 @property
95 def user(self):
96 return self.auth_provider.credentials.get('username', None)
Li Ma216550f2013-06-12 11:26:08 -070097
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000098 @property
99 def tenant_name(self):
100 return self.auth_provider.credentials.get('tenant_name', None)
chris fattarsi5098fa22012-04-17 13:27:00 -0700101
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000102 @property
103 def password(self):
104 return self.auth_provider.credentials.get('password', None)
105
106 @property
107 def base_url(self):
108 return self.auth_provider.base_url(filters=self.filters)
109
110 @property
111 def filters(self):
112 _filters = dict(
113 service=self.service,
114 endpoint_type=self.endpoint_url,
115 region=self._get_region(self.service)
116 )
117 if self.api_version is not None:
118 _filters['api_version'] = self.api_version
119 if self._skip_path:
120 _filters['skip_path'] = self._skip_path
121 return _filters
122
123 def skip_path(self):
chris fattarsi5098fa22012-04-17 13:27:00 -0700124 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000125 When set, ignore the path part of the base URL from the catalog
chris fattarsi5098fa22012-04-17 13:27:00 -0700126 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000127 self._skip_path = True
chris fattarsi5098fa22012-04-17 13:27:00 -0700128
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000129 def reset_path(self):
Attila Fazekasb2902af2013-02-16 16:22:44 +0100130 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000131 When reset, use the base URL from the catalog as-is
Daryl Walleck1465d612011-11-02 02:22:15 -0500132 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000133 self._skip_path = False
Brant Knudsonc7ca3342013-03-28 21:08:50 -0500134
Attila Fazekas54a42862013-07-28 22:31:06 +0200135 def expected_success(self, expected_code, read_code):
136 assert_msg = ("This function only allowed to use for HTTP status"
137 "codes which explicitly defined in the RFC 2616. {0}"
138 " is not a defined Success Code!").format(expected_code)
139 assert expected_code in HTTP_SUCCESS, assert_msg
140
141 # NOTE(afazekas): the http status code above 400 is processed by
142 # the _error_checker method
143 if read_code < 400 and read_code != expected_code:
144 pattern = """Unexpected http success status code {0},
145 The expected status code is {1}"""
146 details = pattern.format(read_code, expected_code)
147 raise exceptions.InvalidHttpSuccessCode(details)
148
Daryl Walleck1465d612011-11-02 02:22:15 -0500149 def post(self, url, body, headers):
150 return self.request('POST', url, headers, body)
151
Attila Fazekasb8aa7592013-01-26 01:25:45 +0100152 def get(self, url, headers=None):
153 return self.request('GET', url, headers)
Daryl Walleck1465d612011-11-02 02:22:15 -0500154
Daisuke Morita499bba32013-11-28 18:44:49 +0900155 def delete(self, url, headers=None, body=None):
156 return self.request('DELETE', url, headers, body)
Daryl Walleck1465d612011-11-02 02:22:15 -0500157
rajalakshmi-ganesanab426722013-02-08 15:49:15 +0530158 def patch(self, url, body, headers):
159 return self.request('PATCH', url, headers, body)
160
Daryl Walleck1465d612011-11-02 02:22:15 -0500161 def put(self, url, body, headers):
162 return self.request('PUT', url, headers, body)
163
dwalleck5d734432012-10-04 01:11:47 -0500164 def head(self, url, headers=None):
Larisa Ustalov6c3c7802012-11-05 12:25:19 +0200165 return self.request('HEAD', url, headers)
166
167 def copy(self, url, headers=None):
168 return self.request('COPY', url, headers)
dwalleck5d734432012-10-04 01:11:47 -0500169
Matthew Treinishc0f768f2013-03-11 14:24:16 -0400170 def get_versions(self):
171 resp, body = self.get('')
172 body = self._parse_resp(body)
173 body = body['versions']
174 versions = map(lambda x: x['id'], body)
175 return resp, versions
176
Attila Fazekas11d2a772013-01-29 17:46:52 +0100177 def _log_request(self, method, req_url, headers, body):
178 self.LOG.info('Request: ' + method + ' ' + req_url)
179 if headers:
180 print_headers = headers
181 if 'X-Auth-Token' in headers and headers['X-Auth-Token']:
182 token = headers['X-Auth-Token']
183 if len(token) > 64 and TOKEN_CHARS_RE.match(token):
184 print_headers = headers.copy()
185 print_headers['X-Auth-Token'] = "<Token omitted>"
186 self.LOG.debug('Request Headers: ' + str(print_headers))
187 if body:
188 str_body = str(body)
189 length = len(str_body)
190 self.LOG.debug('Request Body: ' + str_body[:2048])
191 if length >= 2048:
192 self.LOG.debug("Large body (%d) md5 summary: %s", length,
193 hashlib.md5(str_body).hexdigest())
194
195 def _log_response(self, resp, resp_body):
196 status = resp['status']
197 self.LOG.info("Response Status: " + status)
198 headers = resp.copy()
199 del headers['status']
Matthew Treinishe5423912013-08-13 18:07:31 -0400200 if headers.get('x-compute-request-id'):
201 self.LOG.info("Nova request id: %s" %
202 headers.pop('x-compute-request-id'))
203 elif headers.get('x-openstack-request-id'):
204 self.LOG.info("Glance request id %s" %
205 headers.pop('x-openstack-request-id'))
Attila Fazekas11d2a772013-01-29 17:46:52 +0100206 if len(headers):
207 self.LOG.debug('Response Headers: ' + str(headers))
208 if resp_body:
209 str_body = str(resp_body)
210 length = len(str_body)
211 self.LOG.debug('Response Body: ' + str_body[:2048])
212 if length >= 2048:
213 self.LOG.debug("Large body (%d) md5 summary: %s", length,
214 hashlib.md5(str_body).hexdigest())
Daryl Walleck8a707db2012-01-25 00:46:24 -0600215
Dan Smithba6cb162012-08-14 07:22:42 -0700216 def _parse_resp(self, body):
217 return json.loads(body)
218
Attila Fazekas836e4782013-01-29 15:40:13 +0100219 def response_checker(self, method, url, headers, body, resp, resp_body):
220 if (resp.status in set((204, 205, 304)) or resp.status < 200 or
Pavel Sedláke267eba2013-04-03 15:56:36 +0200221 method.upper() == 'HEAD') and resp_body:
Attila Fazekas836e4782013-01-29 15:40:13 +0100222 raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
Attila Fazekasc3a095b2013-08-17 09:15:44 +0200223 # NOTE(afazekas):
Attila Fazekas836e4782013-01-29 15:40:13 +0100224 # If the HTTP Status Code is 205
225 # 'The response MUST NOT include an entity.'
226 # A HTTP entity has an entity-body and an 'entity-header'.
227 # In the HTTP response specification (Section 6) the 'entity-header'
228 # 'generic-header' and 'response-header' are in OR relation.
229 # All headers not in the above two group are considered as entity
230 # header in every interpretation.
231
232 if (resp.status == 205 and
233 0 != len(set(resp.keys()) - set(('status',)) -
234 self.response_header_lc - self.general_header_lc)):
235 raise exceptions.ResponseWithEntity()
Attila Fazekasc3a095b2013-08-17 09:15:44 +0200236 # NOTE(afazekas)
Attila Fazekas836e4782013-01-29 15:40:13 +0100237 # Now the swift sometimes (delete not empty container)
238 # returns with non json error response, we can create new rest class
239 # for swift.
240 # Usually RFC2616 says error responses SHOULD contain an explanation.
241 # The warning is normal for SHOULD/SHOULD NOT case
242
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100243 # Likely it will cause an error
244 if not resp_body and resp.status >= 400:
Attila Fazekas11d2a772013-01-29 17:46:52 +0100245 self.LOG.warning("status >= 400 response with empty body")
Attila Fazekas836e4782013-01-29 15:40:13 +0100246
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100247 def _request(self, method, url,
248 headers=None, body=None):
Daryl Wallecke5b83d42011-11-10 14:39:02 -0600249 """A simple HTTP request interface."""
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000250 # Authenticate the request with the auth provider
251 req_url, req_headers, req_body = self.auth_provider.auth_request(
252 method, url, headers, body, self.filters)
253 self._log_request(method, req_url, req_headers, req_body)
254 # Do the actual request
255 resp, resp_body = self.http_obj.request(
256 req_url, method, headers=req_headers, body=req_body)
Attila Fazekas11d2a772013-01-29 17:46:52 +0100257 self._log_response(resp, resp_body)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000258 # Verify HTTP response codes
259 self.response_checker(method, url, req_headers, req_body, resp,
260 resp_body)
Attila Fazekas72c7a5f2012-12-03 17:17:23 +0100261
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100262 return resp, resp_body
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500263
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100264 def request(self, method, url,
265 headers=None, body=None):
266 retry = 0
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100267
268 if headers is None:
269 headers = {}
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100270
271 resp, resp_body = self._request(method, url,
272 headers=headers, body=body)
273
274 while (resp.status == 413 and
275 'retry-after' in resp and
276 not self.is_absolute_limit(
277 resp, self._parse_resp(resp_body)) and
278 retry < MAX_RECURSION_DEPTH):
279 retry += 1
280 delay = int(resp['retry-after'])
281 time.sleep(delay)
282 resp, resp_body = self._request(method, url,
283 headers=headers, body=body)
284 self._error_checker(method, url, headers, body,
285 resp, resp_body)
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500286 return resp, resp_body
287
288 def _error_checker(self, method, url,
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100289 headers, body, resp, resp_body):
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500290
291 # NOTE(mtreinish): Check for httplib response from glance_http. The
292 # object can't be used here because importing httplib breaks httplib2.
293 # If another object from a class not imported were passed here as
294 # resp this could possibly fail
295 if str(type(resp)) == "<type 'instance'>":
296 ctype = resp.getheader('content-type')
297 else:
298 try:
299 ctype = resp['content-type']
300 # NOTE(mtreinish): Keystone delete user responses doesn't have a
301 # content-type header. (They don't have a body) So just pretend it
302 # is set.
303 except KeyError:
304 ctype = 'application/json'
305
Attila Fazekase72b7cd2013-03-26 18:34:21 +0100306 # It is not an error response
307 if resp.status < 400:
308 return
309
Sergey Murashovc10cca52014-01-16 12:48:47 +0400310 JSON_ENC = ['application/json', 'application/json; charset=utf-8']
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500311 # NOTE(mtreinish): This is for compatibility with Glance and swift
312 # APIs. These are the return content types that Glance api v1
313 # (and occasionally swift) are using.
Sergey Murashovc10cca52014-01-16 12:48:47 +0400314 TXT_ENC = ['text/plain', 'text/html', 'text/html; charset=utf-8',
315 'text/plain; charset=utf-8']
316 XML_ENC = ['application/xml', 'application/xml; charset=utf-8']
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500317
Sergey Murashovc10cca52014-01-16 12:48:47 +0400318 if ctype.lower() in JSON_ENC or ctype.lower() in XML_ENC:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500319 parse_resp = True
Sergey Murashovc10cca52014-01-16 12:48:47 +0400320 elif ctype.lower() in TXT_ENC:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500321 parse_resp = False
322 else:
323 raise exceptions.RestClientException(str(resp.status))
324
Rohit Karajgi6b1e1542012-05-14 05:55:54 -0700325 if resp.status == 401 or resp.status == 403:
Daryl Walleckced8eb82012-03-19 13:52:37 -0500326 raise exceptions.Unauthorized()
Jay Pipes5135bfc2012-01-05 15:46:49 -0500327
328 if resp.status == 404:
Daryl Walleck8a707db2012-01-25 00:46:24 -0600329 raise exceptions.NotFound(resp_body)
Jay Pipes5135bfc2012-01-05 15:46:49 -0500330
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600331 if resp.status == 400:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500332 if parse_resp:
333 resp_body = self._parse_resp(resp_body)
David Kranz28e35c52012-07-10 10:14:38 -0400334 raise exceptions.BadRequest(resp_body)
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600335
David Kranz5a23d862012-02-14 09:48:55 -0500336 if resp.status == 409:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500337 if parse_resp:
338 resp_body = self._parse_resp(resp_body)
Anju5c3e510c2013-10-18 06:40:29 +0530339 raise exceptions.Conflict(resp_body)
David Kranz5a23d862012-02-14 09:48:55 -0500340
Daryl Wallecked8bef32011-12-05 23:02:08 -0600341 if resp.status == 413:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500342 if parse_resp:
343 resp_body = self._parse_resp(resp_body)
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100344 if self.is_absolute_limit(resp, resp_body):
345 raise exceptions.OverLimit(resp_body)
346 else:
347 raise exceptions.RateLimitExceeded(resp_body)
Brian Lamar12d9b292011-12-08 12:41:21 -0500348
Wangpana9b54c62013-02-28 11:04:32 +0800349 if resp.status == 422:
350 if parse_resp:
351 resp_body = self._parse_resp(resp_body)
352 raise exceptions.UnprocessableEntity(resp_body)
353
Daryl Wallecked8bef32011-12-05 23:02:08 -0600354 if resp.status in (500, 501):
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500355 message = resp_body
356 if parse_resp:
Rohan Kanade433994a2013-12-05 22:34:07 +0530357 try:
358 resp_body = self._parse_resp(resp_body)
359 except ValueError:
360 # If response body is a non-json string message.
361 # Use resp_body as is and raise InvalidResponseBody
362 # exception.
363 raise exceptions.InvalidHTTPResponseBody(message)
364 else:
365 # I'm seeing both computeFault
366 # and cloudServersFault come back.
367 # Will file a bug to fix, but leave as is for now.
368 if 'cloudServersFault' in resp_body:
369 message = resp_body['cloudServersFault']['message']
370 elif 'computeFault' in resp_body:
371 message = resp_body['computeFault']['message']
372 elif 'error' in resp_body: # Keystone errors
373 message = resp_body['error']['message']
374 raise exceptions.IdentityError(message)
375 elif 'message' in resp_body:
376 message = resp_body['message']
Dan Princea4b709c2012-10-10 12:27:59 -0400377
Anju5c3e510c2013-10-18 06:40:29 +0530378 raise exceptions.ServerFault(message)
Daryl Wallecked8bef32011-12-05 23:02:08 -0600379
David Kranz5a23d862012-02-14 09:48:55 -0500380 if resp.status >= 400:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500381 if parse_resp:
382 resp_body = self._parse_resp(resp_body)
Attila Fazekas96524032013-01-29 19:52:49 +0100383 raise exceptions.RestClientException(str(resp.status))
David Kranz5a23d862012-02-14 09:48:55 -0500384
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100385 def is_absolute_limit(self, resp, resp_body):
386 if (not isinstance(resp_body, collections.Mapping) or
Pavel Sedláke267eba2013-04-03 15:56:36 +0200387 'retry-after' not in resp):
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100388 return True
389 over_limit = resp_body.get('overLimit', None)
390 if not over_limit:
391 return True
392 return 'exceed' in over_limit.get('message', 'blabla')
rajalakshmi-ganesan0275a0d2013-01-11 18:26:05 +0530393
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
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100419 def is_absolute_limit(self, resp, resp_body):
420 if (not isinstance(resp_body, collections.Mapping) or
Pavel Sedláke267eba2013-04-03 15:56:36 +0200421 'retry-after' not in resp):
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100422 return True
423 return 'exceed' in resp_body.get('message', 'blabla')
Marc Koderer24eb89c2014-01-31 11:23:33 +0100424
425
426class NegativeRestClient(RestClient):
427 """
428 Version of RestClient that does not raise exceptions.
429 """
430 def _error_checker(self, method, url,
431 headers, body, resp, resp_body):
432 pass
433
434 def send_request(self, method, url_template, resources, body=None):
435 url = url_template % tuple(resources)
436 if method == "GET":
437 resp, body = self.get(url)
438 elif method == "POST":
439 resp, body = self.post(url, body, self.headers)
440 elif method == "PUT":
441 resp, body = self.put(url, body, self.headers)
442 elif method == "PATCH":
443 resp, body = self.patch(url, body, self.headers)
444 elif method == "HEAD":
445 resp, body = self.head(url)
446 elif method == "DELETE":
447 resp, body = self.delete(url)
448 elif method == "COPY":
449 resp, body = self.copy(url)
450 else:
451 assert False
452
453 return resp, body