blob: 00fe8d2c798123e1fc60f6ed96d29249ca26bed3 [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
Matthew Treinisha83a16e2012-12-07 13:44:02 -050018import json
Attila Fazekas11d2a772013-01-29 17:46:52 +010019import re
Eoghan Glynna5598972012-03-01 09:27:17 -050020import time
Jay Pipes3f981df2012-03-27 18:59:44 -040021
Chris Yeohc266b282014-03-13 18:19:00 +103022import jsonschema
Matthew Treinish96e9e882014-06-09 18:37:19 -040023from lxml import etree
Sean Dague4f8d7022014-09-25 10:27:13 -040024import six
Chris Yeohc266b282014-03-13 18:19:00 +103025
Mate Lakat23a58a32013-08-23 02:06:22 +010026from tempest.common import http
Matt Riedemann7efa5c32014-05-02 13:35:44 -070027from tempest.common.utils import misc as misc_utils
Matthew Treinish28f164c2014-03-04 18:55:06 +000028from tempest.common import xml_utils as common
Matthew Treinish684d8992014-01-30 16:27:40 +000029from tempest import config
Daryl Wallecked8bef32011-12-05 23:02:08 -060030from tempest import exceptions
Matthew Treinishf4a9b0f2013-07-26 16:58:26 -040031from tempest.openstack.common import log as logging
Daryl Walleck1465d612011-11-02 02:22:15 -050032
Matthew Treinish684d8992014-01-30 16:27:40 +000033CONF = config.CONF
34
Eoghan Glynna5598972012-03-01 09:27:17 -050035# redrive rate limited calls at most twice
36MAX_RECURSION_DEPTH = 2
Attila Fazekas11d2a772013-01-29 17:46:52 +010037TOKEN_CHARS_RE = re.compile('^[-A-Za-z0-9+/=]*$')
Eoghan Glynna5598972012-03-01 09:27:17 -050038
Attila Fazekas54a42862013-07-28 22:31:06 +020039# All the successful HTTP status codes from RFC 2616
40HTTP_SUCCESS = (200, 201, 202, 203, 204, 205, 206)
41
Eoghan Glynna5598972012-03-01 09:27:17 -050042
Sean Dague4f8d7022014-09-25 10:27:13 -040043# convert a structure into a string safely
44def safe_body(body, maxlen=2048):
45 try:
46 text = six.text_type(body)
47 except UnicodeDecodeError:
48 # if this isn't actually text, return marker that
49 return "<BinaryData: removed>"
50 if len(text) > maxlen:
51 return text[:maxlen]
52 else:
53 return text
54
55
Daryl Walleck1465d612011-11-02 02:22:15 -050056class RestClient(object):
vponomaryov67b58fe2014-02-06 19:05:41 +020057
Dan Smithba6cb162012-08-14 07:22:42 -070058 TYPE = "json"
vponomaryov67b58fe2014-02-06 19:05:41 +020059
60 # This is used by _parse_resp method
61 # Redefine it for purposes of your xml service client
62 # List should contain top-xml_tag-names of data, which is like list/array
63 # For example, in keystone it is users, roles, tenants and services
64 # All of it has children with same tag-names
65 list_tags = []
66
67 # This is used by _parse_resp method too
68 # Used for selection of dict-like xmls,
69 # like metadata for Vms in nova, and volumes in cinder
70 dict_tags = ["metadata", ]
71
Attila Fazekas11d2a772013-01-29 17:46:52 +010072 LOG = logging.getLogger(__name__)
Daryl Walleck1465d612011-11-02 02:22:15 -050073
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000074 def __init__(self, auth_provider):
75 self.auth_provider = auth_provider
chris fattarsi5098fa22012-04-17 13:27:00 -070076
JordanP5d29b2c2013-12-18 13:56:03 +000077 self.endpoint_url = None
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000078 self.service = None
79 # The version of the API this client implements
80 self.api_version = None
81 self._skip_path = False
Matthew Treinish684d8992014-01-30 16:27:40 +000082 self.build_interval = CONF.compute.build_interval
83 self.build_timeout = CONF.compute.build_timeout
Attila Fazekas72c7a5f2012-12-03 17:17:23 +010084 self.general_header_lc = set(('cache-control', 'connection',
85 'date', 'pragma', 'trailer',
86 'transfer-encoding', 'via',
87 'warning'))
88 self.response_header_lc = set(('accept-ranges', 'age', 'etag',
89 'location', 'proxy-authenticate',
90 'retry-after', 'server',
91 'vary', 'www-authenticate'))
Matthew Treinish684d8992014-01-30 16:27:40 +000092 dscv = CONF.identity.disable_ssl_certificate_validation
Mate Lakat23a58a32013-08-23 02:06:22 +010093 self.http_obj = http.ClosingHttp(
94 disable_ssl_certificate_validation=dscv)
chris fattarsi5098fa22012-04-17 13:27:00 -070095
vponomaryov67b58fe2014-02-06 19:05:41 +020096 def _get_type(self):
97 return self.TYPE
98
99 def get_headers(self, accept_type=None, send_type=None):
vponomaryov67b58fe2014-02-06 19:05:41 +0200100 if accept_type is None:
101 accept_type = self._get_type()
102 if send_type is None:
103 send_type = self._get_type()
104 return {'Content-Type': 'application/%s' % send_type,
105 'Accept': 'application/%s' % accept_type}
106
DennyZhang7be75002013-09-19 06:55:11 -0500107 def __str__(self):
108 STRING_LIMIT = 80
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000109 str_format = ("config:%s, service:%s, base_url:%s, "
110 "filters: %s, build_interval:%s, build_timeout:%s"
DennyZhang7be75002013-09-19 06:55:11 -0500111 "\ntoken:%s..., \nheaders:%s...")
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000112 return str_format % (CONF, self.service, self.base_url,
113 self.filters, self.build_interval,
114 self.build_timeout,
DennyZhang7be75002013-09-19 06:55:11 -0500115 str(self.token)[0:STRING_LIMIT],
vponomaryov67b58fe2014-02-06 19:05:41 +0200116 str(self.get_headers())[0:STRING_LIMIT])
DennyZhang7be75002013-09-19 06:55:11 -0500117
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000118 def _get_region(self, service):
chris fattarsi5098fa22012-04-17 13:27:00 -0700119 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000120 Returns the region for a specific service
chris fattarsi5098fa22012-04-17 13:27:00 -0700121 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000122 service_region = None
123 for cfgname in dir(CONF._config):
124 # Find all config.FOO.catalog_type and assume FOO is a service.
125 cfg = getattr(CONF, cfgname)
126 catalog_type = getattr(cfg, 'catalog_type', None)
127 if catalog_type == service:
128 service_region = getattr(cfg, 'region', None)
129 if not service_region:
130 service_region = CONF.identity.region
131 return service_region
chris fattarsi5098fa22012-04-17 13:27:00 -0700132
JordanP5d29b2c2013-12-18 13:56:03 +0000133 def _get_endpoint_type(self, service):
134 """
135 Returns the endpoint type for a specific service
136 """
137 # If the client requests a specific endpoint type, then be it
138 if self.endpoint_url:
139 return self.endpoint_url
140 endpoint_type = None
141 for cfgname in dir(CONF._config):
142 # Find all config.FOO.catalog_type and assume FOO is a service.
143 cfg = getattr(CONF, cfgname)
144 catalog_type = getattr(cfg, 'catalog_type', None)
145 if catalog_type == service:
146 endpoint_type = getattr(cfg, 'endpoint_type', 'publicURL')
147 break
148 # Special case for compute v3 service which hasn't its own
149 # configuration group
150 else:
151 if service == CONF.compute.catalog_v3_type:
152 endpoint_type = CONF.compute.endpoint_type
153 return endpoint_type
154
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000155 @property
156 def user(self):
Andrea Frittoli86ad28d2014-03-20 10:09:12 +0000157 return self.auth_provider.credentials.username
Li Ma216550f2013-06-12 11:26:08 -0700158
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000159 @property
Andrea Frittoli9612e812014-03-13 10:57:26 +0000160 def user_id(self):
161 return self.auth_provider.credentials.user_id
162
163 @property
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000164 def tenant_name(self):
Andrea Frittoli86ad28d2014-03-20 10:09:12 +0000165 return self.auth_provider.credentials.tenant_name
166
167 @property
168 def tenant_id(self):
169 return self.auth_provider.credentials.tenant_id
chris fattarsi5098fa22012-04-17 13:27:00 -0700170
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000171 @property
172 def password(self):
Andrea Frittoli86ad28d2014-03-20 10:09:12 +0000173 return self.auth_provider.credentials.password
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000174
175 @property
176 def base_url(self):
177 return self.auth_provider.base_url(filters=self.filters)
178
179 @property
Andrea Frittoli77f9da42014-02-06 11:18:19 +0000180 def token(self):
181 return self.auth_provider.get_token()
182
183 @property
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000184 def filters(self):
185 _filters = dict(
186 service=self.service,
JordanP5d29b2c2013-12-18 13:56:03 +0000187 endpoint_type=self._get_endpoint_type(self.service),
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000188 region=self._get_region(self.service)
189 )
190 if self.api_version is not None:
191 _filters['api_version'] = self.api_version
192 if self._skip_path:
193 _filters['skip_path'] = self._skip_path
194 return _filters
195
196 def skip_path(self):
chris fattarsi5098fa22012-04-17 13:27:00 -0700197 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000198 When set, ignore the path part of the base URL from the catalog
chris fattarsi5098fa22012-04-17 13:27:00 -0700199 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000200 self._skip_path = True
chris fattarsi5098fa22012-04-17 13:27:00 -0700201
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000202 def reset_path(self):
Attila Fazekasb2902af2013-02-16 16:22:44 +0100203 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000204 When reset, use the base URL from the catalog as-is
Daryl Walleck1465d612011-11-02 02:22:15 -0500205 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000206 self._skip_path = False
Brant Knudsonc7ca3342013-03-28 21:08:50 -0500207
Matthew Treinish2b2483e2014-05-08 23:26:10 -0400208 @classmethod
209 def expected_success(cls, expected_code, read_code):
Attila Fazekas54a42862013-07-28 22:31:06 +0200210 assert_msg = ("This function only allowed to use for HTTP status"
211 "codes which explicitly defined in the RFC 2616. {0}"
212 " is not a defined Success Code!").format(expected_code)
Matthew Treinish2b2483e2014-05-08 23:26:10 -0400213 if isinstance(expected_code, list):
214 for code in expected_code:
215 assert code in HTTP_SUCCESS, assert_msg
216 else:
217 assert expected_code in HTTP_SUCCESS, assert_msg
Attila Fazekas54a42862013-07-28 22:31:06 +0200218
219 # NOTE(afazekas): the http status code above 400 is processed by
220 # the _error_checker method
Matthew Treinish2b2483e2014-05-08 23:26:10 -0400221 if read_code < 400:
222 pattern = """Unexpected http success status code {0},
223 The expected status code is {1}"""
224 if ((not isinstance(expected_code, list) and
Matthew Treinish1d14c542014-06-17 20:25:40 -0400225 (read_code != expected_code)) or
226 (isinstance(expected_code, list) and
227 (read_code not in expected_code))):
Attila Fazekas54a42862013-07-28 22:31:06 +0200228 details = pattern.format(read_code, expected_code)
229 raise exceptions.InvalidHttpSuccessCode(details)
230
Sergey Murashov4fccd322014-03-22 09:58:52 +0400231 def post(self, url, body, headers=None, extra_headers=False):
232 return self.request('POST', url, extra_headers, headers, body)
Daryl Walleck1465d612011-11-02 02:22:15 -0500233
Sergey Murashov4fccd322014-03-22 09:58:52 +0400234 def get(self, url, headers=None, extra_headers=False):
235 return self.request('GET', url, extra_headers, headers)
Daryl Walleck1465d612011-11-02 02:22:15 -0500236
Sergey Murashov4fccd322014-03-22 09:58:52 +0400237 def delete(self, url, headers=None, body=None, extra_headers=False):
238 return self.request('DELETE', url, extra_headers, headers, body)
Daryl Walleck1465d612011-11-02 02:22:15 -0500239
Sergey Murashov4fccd322014-03-22 09:58:52 +0400240 def patch(self, url, body, headers=None, extra_headers=False):
241 return self.request('PATCH', url, extra_headers, headers, body)
rajalakshmi-ganesanab426722013-02-08 15:49:15 +0530242
Sergey Murashov4fccd322014-03-22 09:58:52 +0400243 def put(self, url, body, headers=None, extra_headers=False):
244 return self.request('PUT', url, extra_headers, headers, body)
Daryl Walleck1465d612011-11-02 02:22:15 -0500245
Sergey Murashov4fccd322014-03-22 09:58:52 +0400246 def head(self, url, headers=None, extra_headers=False):
247 return self.request('HEAD', url, extra_headers, headers)
Larisa Ustalov6c3c7802012-11-05 12:25:19 +0200248
Sergey Murashov4fccd322014-03-22 09:58:52 +0400249 def copy(self, url, headers=None, extra_headers=False):
250 return self.request('COPY', url, extra_headers, headers)
dwalleck5d734432012-10-04 01:11:47 -0500251
Matthew Treinishc0f768f2013-03-11 14:24:16 -0400252 def get_versions(self):
253 resp, body = self.get('')
254 body = self._parse_resp(body)
Matthew Treinishc0f768f2013-03-11 14:24:16 -0400255 versions = map(lambda x: x['id'], body)
256 return resp, versions
257
Sean Dague89a85912014-03-19 16:37:29 -0400258 def _get_request_id(self, resp):
259 for i in ('x-openstack-request-id', 'x-compute-request-id'):
260 if i in resp:
261 return resp[i]
262 return ""
Attila Fazekas11d2a772013-01-29 17:46:52 +0100263
Ghanshyam2a180b82014-06-16 13:54:22 +0900264 def _log_request_start(self, method, req_url, req_headers=None,
Sean Dague2cb56992014-05-29 08:17:42 -0400265 req_body=None):
Ghanshyam2a180b82014-06-16 13:54:22 +0900266 if req_headers is None:
267 req_headers = {}
Sean Dague2cb56992014-05-29 08:17:42 -0400268 caller_name = misc_utils.find_test_caller()
269 trace_regex = CONF.debug.trace_requests
270 if trace_regex and re.search(trace_regex, caller_name):
271 self.LOG.debug('Starting Request (%s): %s %s' %
272 (caller_name, method, req_url))
273
Sean Dague4f8d7022014-09-25 10:27:13 -0400274 def _log_request_full(self, method, req_url, resp,
275 secs="", req_headers=None,
276 req_body=None, resp_body=None,
277 caller_name=None, extra=None):
278 if 'X-Auth-Token' in req_headers:
279 req_headers['X-Auth-Token'] = '<omitted>'
280 log_fmt = """Request (%s): %s %s %s%s
281 Request - Headers: %s
282 Body: %s
283 Response - Headers: %s
284 Body: %s"""
285
286 self.LOG.debug(
287 log_fmt % (
288 caller_name,
289 resp['status'],
290 method,
291 req_url,
292 secs,
293 str(req_headers),
294 safe_body(req_body),
295 str(resp),
296 safe_body(resp_body)),
297 extra=extra)
298
Sean Daguec522c092014-03-24 10:43:22 -0400299 def _log_request(self, method, req_url, resp,
Ghanshyam2a180b82014-06-16 13:54:22 +0900300 secs="", req_headers=None,
Sean Daguec522c092014-03-24 10:43:22 -0400301 req_body=None, resp_body=None):
Ghanshyam2a180b82014-06-16 13:54:22 +0900302 if req_headers is None:
303 req_headers = {}
Sean Dague0cc47572014-03-20 07:34:05 -0400304 # if we have the request id, put it in the right part of the log
Sean Dague89a85912014-03-19 16:37:29 -0400305 extra = dict(request_id=self._get_request_id(resp))
Sean Dague0cc47572014-03-20 07:34:05 -0400306 # NOTE(sdague): while we still have 6 callers to this function
307 # we're going to just provide work around on who is actually
308 # providing timings by gracefully adding no content if they don't.
309 # Once we're down to 1 caller, clean this up.
Matt Riedemann7efa5c32014-05-02 13:35:44 -0700310 caller_name = misc_utils.find_test_caller()
Sean Dague0cc47572014-03-20 07:34:05 -0400311 if secs:
312 secs = " %.3fs" % secs
Sean Dague89a85912014-03-19 16:37:29 -0400313 self.LOG.info(
Sean Dague0cc47572014-03-20 07:34:05 -0400314 'Request (%s): %s %s %s%s' % (
Sean Daguec522c092014-03-24 10:43:22 -0400315 caller_name,
Sean Dague89a85912014-03-19 16:37:29 -0400316 resp['status'],
317 method,
Sean Dague0cc47572014-03-20 07:34:05 -0400318 req_url,
319 secs),
Sean Dague89a85912014-03-19 16:37:29 -0400320 extra=extra)
Daryl Walleck8a707db2012-01-25 00:46:24 -0600321
Sean Dague4f8d7022014-09-25 10:27:13 -0400322 # Also look everything at DEBUG if you want to filter this
323 # out, don't run at debug.
324 self._log_request_full(method, req_url, resp, secs, req_headers,
325 req_body, resp_body, caller_name, extra)
Sean Daguec522c092014-03-24 10:43:22 -0400326
Dan Smithba6cb162012-08-14 07:22:42 -0700327 def _parse_resp(self, body):
vponomaryov67b58fe2014-02-06 19:05:41 +0200328 if self._get_type() is "json":
329 body = json.loads(body)
330
331 # We assume, that if the first value of the deserialized body's
332 # item set is a dict or a list, that we just return the first value
333 # of deserialized body.
334 # Essentially "cutting out" the first placeholder element in a body
335 # that looks like this:
336 #
337 # {
338 # "users": [
339 # ...
340 # ]
341 # }
342 try:
343 # Ensure there are not more than one top-level keys
344 if len(body.keys()) > 1:
345 return body
346 # Just return the "wrapped" element
347 first_key, first_item = body.items()[0]
348 if isinstance(first_item, (dict, list)):
349 return first_item
350 except (ValueError, IndexError):
351 pass
352 return body
353 elif self._get_type() is "xml":
354 element = etree.fromstring(body)
355 if any(s in element.tag for s in self.dict_tags):
356 # Parse dictionary-like xmls (metadata, etc)
357 dictionary = {}
358 for el in element.getchildren():
359 dictionary[u"%s" % el.get("key")] = u"%s" % el.text
360 return dictionary
361 if any(s in element.tag for s in self.list_tags):
362 # Parse list-like xmls (users, roles, etc)
363 array = []
364 for child in element.getchildren():
Masayuki Igawa1edf94f2014-03-04 18:34:16 +0900365 array.append(common.xml_to_json(child))
vponomaryov67b58fe2014-02-06 19:05:41 +0200366 return array
367
368 # Parse one-item-like xmls (user, role, etc)
Masayuki Igawa1edf94f2014-03-04 18:34:16 +0900369 return common.xml_to_json(element)
Dan Smithba6cb162012-08-14 07:22:42 -0700370
Yaroslav Lobankovaede3802014-04-23 17:18:53 +0400371 def response_checker(self, method, resp, resp_body):
Attila Fazekas836e4782013-01-29 15:40:13 +0100372 if (resp.status in set((204, 205, 304)) or resp.status < 200 or
Pavel Sedláke267eba2013-04-03 15:56:36 +0200373 method.upper() == 'HEAD') and resp_body:
Attila Fazekas836e4782013-01-29 15:40:13 +0100374 raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
Attila Fazekasc3a095b2013-08-17 09:15:44 +0200375 # NOTE(afazekas):
Attila Fazekas836e4782013-01-29 15:40:13 +0100376 # If the HTTP Status Code is 205
377 # 'The response MUST NOT include an entity.'
378 # A HTTP entity has an entity-body and an 'entity-header'.
379 # In the HTTP response specification (Section 6) the 'entity-header'
380 # 'generic-header' and 'response-header' are in OR relation.
381 # All headers not in the above two group are considered as entity
382 # header in every interpretation.
383
384 if (resp.status == 205 and
385 0 != len(set(resp.keys()) - set(('status',)) -
386 self.response_header_lc - self.general_header_lc)):
387 raise exceptions.ResponseWithEntity()
Attila Fazekasc3a095b2013-08-17 09:15:44 +0200388 # NOTE(afazekas)
Attila Fazekas836e4782013-01-29 15:40:13 +0100389 # Now the swift sometimes (delete not empty container)
390 # returns with non json error response, we can create new rest class
391 # for swift.
392 # Usually RFC2616 says error responses SHOULD contain an explanation.
393 # The warning is normal for SHOULD/SHOULD NOT case
394
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100395 # Likely it will cause an error
Sean Daguec9a94f92014-06-23 08:31:50 -0400396 if method != 'HEAD' and not resp_body and resp.status >= 400:
Attila Fazekas11d2a772013-01-29 17:46:52 +0100397 self.LOG.warning("status >= 400 response with empty body")
Attila Fazekas836e4782013-01-29 15:40:13 +0100398
vponomaryov67b58fe2014-02-06 19:05:41 +0200399 def _request(self, method, url, headers=None, body=None):
Daryl Wallecke5b83d42011-11-10 14:39:02 -0600400 """A simple HTTP request interface."""
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000401 # Authenticate the request with the auth provider
402 req_url, req_headers, req_body = self.auth_provider.auth_request(
403 method, url, headers, body, self.filters)
Sean Dague89a85912014-03-19 16:37:29 -0400404
Sean Dague0cc47572014-03-20 07:34:05 -0400405 # Do the actual request, and time it
406 start = time.time()
Sean Dague2cb56992014-05-29 08:17:42 -0400407 self._log_request_start(method, req_url)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000408 resp, resp_body = self.http_obj.request(
409 req_url, method, headers=req_headers, body=req_body)
Sean Dague0cc47572014-03-20 07:34:05 -0400410 end = time.time()
Sean Daguec522c092014-03-24 10:43:22 -0400411 self._log_request(method, req_url, resp, secs=(end - start),
412 req_headers=req_headers, req_body=req_body,
413 resp_body=resp_body)
414
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000415 # Verify HTTP response codes
Yaroslav Lobankovaede3802014-04-23 17:18:53 +0400416 self.response_checker(method, resp, resp_body)
Attila Fazekas72c7a5f2012-12-03 17:17:23 +0100417
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100418 return resp, resp_body
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500419
Sergey Murashov4fccd322014-03-22 09:58:52 +0400420 def request(self, method, url, extra_headers=False, headers=None,
421 body=None):
422 # if extra_headers is True
423 # default headers would be added to headers
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100424 retry = 0
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100425
426 if headers is None:
vponomaryov67b58fe2014-02-06 19:05:41 +0200427 # NOTE(vponomaryov): if some client do not need headers,
428 # it should explicitly pass empty dict
429 headers = self.get_headers()
Sergey Murashov4fccd322014-03-22 09:58:52 +0400430 elif extra_headers:
431 try:
432 headers = headers.copy()
433 headers.update(self.get_headers())
434 except (ValueError, TypeError):
435 headers = self.get_headers()
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100436
437 resp, resp_body = self._request(method, url,
438 headers=headers, body=body)
439
440 while (resp.status == 413 and
441 'retry-after' in resp and
442 not self.is_absolute_limit(
443 resp, self._parse_resp(resp_body)) and
444 retry < MAX_RECURSION_DEPTH):
445 retry += 1
446 delay = int(resp['retry-after'])
447 time.sleep(delay)
448 resp, resp_body = self._request(method, url,
449 headers=headers, body=body)
450 self._error_checker(method, url, headers, body,
451 resp, resp_body)
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500452 return resp, resp_body
453
454 def _error_checker(self, method, url,
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100455 headers, body, resp, resp_body):
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500456
457 # NOTE(mtreinish): Check for httplib response from glance_http. The
458 # object can't be used here because importing httplib breaks httplib2.
459 # If another object from a class not imported were passed here as
460 # resp this could possibly fail
461 if str(type(resp)) == "<type 'instance'>":
462 ctype = resp.getheader('content-type')
463 else:
464 try:
465 ctype = resp['content-type']
466 # NOTE(mtreinish): Keystone delete user responses doesn't have a
467 # content-type header. (They don't have a body) So just pretend it
468 # is set.
469 except KeyError:
470 ctype = 'application/json'
471
Attila Fazekase72b7cd2013-03-26 18:34:21 +0100472 # It is not an error response
473 if resp.status < 400:
474 return
475
Sergey Murashovc10cca52014-01-16 12:48:47 +0400476 JSON_ENC = ['application/json', 'application/json; charset=utf-8']
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500477 # NOTE(mtreinish): This is for compatibility with Glance and swift
478 # APIs. These are the return content types that Glance api v1
479 # (and occasionally swift) are using.
Sergey Murashovc10cca52014-01-16 12:48:47 +0400480 TXT_ENC = ['text/plain', 'text/html', 'text/html; charset=utf-8',
481 'text/plain; charset=utf-8']
482 XML_ENC = ['application/xml', 'application/xml; charset=utf-8']
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500483
Sergey Murashovc10cca52014-01-16 12:48:47 +0400484 if ctype.lower() in JSON_ENC or ctype.lower() in XML_ENC:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500485 parse_resp = True
Sergey Murashovc10cca52014-01-16 12:48:47 +0400486 elif ctype.lower() in TXT_ENC:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500487 parse_resp = False
488 else:
vponomaryov6cb6d192014-03-07 09:39:05 +0200489 raise exceptions.InvalidContentType(str(resp.status))
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500490
Rohit Karajgi6b1e1542012-05-14 05:55:54 -0700491 if resp.status == 401 or resp.status == 403:
Christian Schwede285a8482014-04-09 06:12:55 +0000492 raise exceptions.Unauthorized(resp_body)
Jay Pipes5135bfc2012-01-05 15:46:49 -0500493
494 if resp.status == 404:
Daryl Walleck8a707db2012-01-25 00:46:24 -0600495 raise exceptions.NotFound(resp_body)
Jay Pipes5135bfc2012-01-05 15:46:49 -0500496
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600497 if resp.status == 400:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500498 if parse_resp:
499 resp_body = self._parse_resp(resp_body)
David Kranz28e35c52012-07-10 10:14:38 -0400500 raise exceptions.BadRequest(resp_body)
Daryl Walleckadea1fa2011-11-15 18:36:39 -0600501
David Kranz5a23d862012-02-14 09:48:55 -0500502 if resp.status == 409:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500503 if parse_resp:
504 resp_body = self._parse_resp(resp_body)
Anju5c3e510c2013-10-18 06:40:29 +0530505 raise exceptions.Conflict(resp_body)
David Kranz5a23d862012-02-14 09:48:55 -0500506
Daryl Wallecked8bef32011-12-05 23:02:08 -0600507 if resp.status == 413:
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500508 if parse_resp:
509 resp_body = self._parse_resp(resp_body)
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100510 if self.is_absolute_limit(resp, resp_body):
511 raise exceptions.OverLimit(resp_body)
512 else:
513 raise exceptions.RateLimitExceeded(resp_body)
Brian Lamar12d9b292011-12-08 12:41:21 -0500514
Wangpana9b54c62013-02-28 11:04:32 +0800515 if resp.status == 422:
516 if parse_resp:
517 resp_body = self._parse_resp(resp_body)
518 raise exceptions.UnprocessableEntity(resp_body)
519
Daryl Wallecked8bef32011-12-05 23:02:08 -0600520 if resp.status in (500, 501):
Matthew Treinish7e5a3ec2013-02-08 13:53:58 -0500521 message = resp_body
522 if parse_resp:
Rohan Kanade433994a2013-12-05 22:34:07 +0530523 try:
524 resp_body = self._parse_resp(resp_body)
525 except ValueError:
526 # If response body is a non-json string message.
527 # Use resp_body as is and raise InvalidResponseBody
528 # exception.
529 raise exceptions.InvalidHTTPResponseBody(message)
530 else:
vponomaryov6cb6d192014-03-07 09:39:05 +0200531 if isinstance(resp_body, dict):
532 # I'm seeing both computeFault
533 # and cloudServersFault come back.
534 # Will file a bug to fix, but leave as is for now.
535 if 'cloudServersFault' in resp_body:
536 message = resp_body['cloudServersFault']['message']
537 elif 'computeFault' in resp_body:
538 message = resp_body['computeFault']['message']
539 elif 'error' in resp_body: # Keystone errors
540 message = resp_body['error']['message']
541 raise exceptions.IdentityError(message)
542 elif 'message' in resp_body:
543 message = resp_body['message']
544 else:
545 message = resp_body
Dan Princea4b709c2012-10-10 12:27:59 -0400546
Anju5c3e510c2013-10-18 06:40:29 +0530547 raise exceptions.ServerFault(message)
Daryl Wallecked8bef32011-12-05 23:02:08 -0600548
David Kranz5a23d862012-02-14 09:48:55 -0500549 if resp.status >= 400:
vponomaryov6cb6d192014-03-07 09:39:05 +0200550 raise exceptions.UnexpectedResponseCode(str(resp.status))
David Kranz5a23d862012-02-14 09:48:55 -0500551
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100552 def is_absolute_limit(self, resp, resp_body):
553 if (not isinstance(resp_body, collections.Mapping) or
Pavel Sedláke267eba2013-04-03 15:56:36 +0200554 'retry-after' not in resp):
Attila Fazekas55f6d8c2013-03-10 10:32:54 +0100555 return True
vponomaryov67b58fe2014-02-06 19:05:41 +0200556 if self._get_type() is "json":
557 over_limit = resp_body.get('overLimit', None)
558 if not over_limit:
559 return True
560 return 'exceed' in over_limit.get('message', 'blabla')
561 elif self._get_type() is "xml":
562 return 'exceed' in resp_body.get('message', 'blabla')
rajalakshmi-ganesan0275a0d2013-01-11 18:26:05 +0530563
David Kranz6aceb4a2012-06-05 14:05:45 -0400564 def wait_for_resource_deletion(self, id):
Sean Daguef237ccb2013-01-04 15:19:14 -0500565 """Waits for a resource to be deleted."""
David Kranz6aceb4a2012-06-05 14:05:45 -0400566 start_time = int(time.time())
567 while True:
568 if self.is_resource_deleted(id):
569 return
570 if int(time.time()) - start_time >= self.build_timeout:
Matt Riedemann30276742014-09-10 11:29:49 -0700571 message = ('Failed to delete resource %(id)s within the '
572 'required time (%(timeout)s s).' %
573 {'id': id, 'timeout': self.build_timeout})
574 caller = misc_utils.find_test_caller()
575 if caller:
576 message = '(%s) %s' % (caller, message)
577 raise exceptions.TimeoutException(message)
David Kranz6aceb4a2012-06-05 14:05:45 -0400578 time.sleep(self.build_interval)
579
580 def is_resource_deleted(self, id):
581 """
582 Subclasses override with specific deletion detection.
583 """
Attila Fazekasd236b4e2013-01-26 00:44:12 +0100584 message = ('"%s" does not implement is_resource_deleted'
585 % self.__class__.__name__)
586 raise NotImplementedError(message)
Dan Smithba6cb162012-08-14 07:22:42 -0700587
Chris Yeohc266b282014-03-13 18:19:00 +1030588 @classmethod
589 def validate_response(cls, schema, resp, body):
590 # Only check the response if the status code is a success code
591 # TODO(cyeoh): Eventually we should be able to verify that a failure
592 # code if it exists is something that we expect. This is explicitly
593 # declared in the V3 API and so we should be able to export this in
594 # the response schema. For now we'll ignore it.
Ken'ichi Ohmichi4e0917c2014-03-19 15:33:47 +0900595 if resp.status in HTTP_SUCCESS:
Matthew Treinish2b2483e2014-05-08 23:26:10 -0400596 cls.expected_success(schema['status_code'], resp.status)
Ken'ichi Ohmichi57b384b2014-03-28 13:58:20 +0900597
598 # Check the body of a response
599 body_schema = schema.get('response_body')
600 if body_schema:
Chris Yeohc266b282014-03-13 18:19:00 +1030601 try:
Ken'ichi Ohmichi57b384b2014-03-28 13:58:20 +0900602 jsonschema.validate(body, body_schema)
Chris Yeohc266b282014-03-13 18:19:00 +1030603 except jsonschema.ValidationError as ex:
604 msg = ("HTTP response body is invalid (%s)") % ex
605 raise exceptions.InvalidHTTPResponseBody(msg)
606 else:
607 if body:
608 msg = ("HTTP response body should not exist (%s)") % body
609 raise exceptions.InvalidHTTPResponseBody(msg)
610
Ken'ichi Ohmichi57b384b2014-03-28 13:58:20 +0900611 # Check the header of a response
612 header_schema = schema.get('response_header')
613 if header_schema:
614 try:
615 jsonschema.validate(resp, header_schema)
616 except jsonschema.ValidationError as ex:
617 msg = ("HTTP response header is invalid (%s)") % ex
618 raise exceptions.InvalidHTTPResponseHeader(msg)
619
Dan Smithba6cb162012-08-14 07:22:42 -0700620
Marc Koderer24eb89c2014-01-31 11:23:33 +0100621class NegativeRestClient(RestClient):
622 """
623 Version of RestClient that does not raise exceptions.
624 """
625 def _error_checker(self, method, url,
626 headers, body, resp, resp_body):
627 pass
628
629 def send_request(self, method, url_template, resources, body=None):
630 url = url_template % tuple(resources)
631 if method == "GET":
632 resp, body = self.get(url)
633 elif method == "POST":
vponomaryov67b58fe2014-02-06 19:05:41 +0200634 resp, body = self.post(url, body)
Marc Koderer24eb89c2014-01-31 11:23:33 +0100635 elif method == "PUT":
vponomaryov67b58fe2014-02-06 19:05:41 +0200636 resp, body = self.put(url, body)
Marc Koderer24eb89c2014-01-31 11:23:33 +0100637 elif method == "PATCH":
vponomaryov67b58fe2014-02-06 19:05:41 +0200638 resp, body = self.patch(url, body)
Marc Koderer24eb89c2014-01-31 11:23:33 +0100639 elif method == "HEAD":
640 resp, body = self.head(url)
641 elif method == "DELETE":
642 resp, body = self.delete(url)
643 elif method == "COPY":
644 resp, body = self.copy(url)
645 else:
646 assert False
647
648 return resp, body