blob: ef14dfc1f77eebfb38b60aab536f83a126c0134e [file] [log] [blame]
Matthew Treinish9e26ca82016-02-23 11:43:20 -05001# Copyright 2012 OpenStack Foundation
2# Copyright 2013 IBM Corp.
3# 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
Takashi Kajinami6354f612021-07-17 00:37:34 +090017from collections import abc
Paul Glass119565a2016-04-06 11:41:42 -050018import email.utils
Matthew Treinish9e26ca82016-02-23 11:43:20 -050019import re
20import time
songwenping99d6e002021-01-05 03:07:46 +000021import urllib
Matthew Treinish9e26ca82016-02-23 11:43:20 -050022
23import jsonschema
24from oslo_log import log as logging
Rodolfo Alonso Hernandezc1449d42020-02-15 13:24:28 +000025from oslo_log import versionutils
Matthew Treinish9e26ca82016-02-23 11:43:20 -050026from oslo_serialization import jsonutils as json
Matthew Treinish9e26ca82016-02-23 11:43:20 -050027
28from tempest.lib.common import http
ghanshyamf9ded352016-04-12 17:03:01 +090029from tempest.lib.common import jsonschema_validator
Ilya Shakhat1291bb42017-11-29 18:08:16 +010030from tempest.lib.common import profiler
Jordan Pittier9e227c52016-02-09 14:35:18 +010031from tempest.lib.common.utils import test_utils
Matthew Treinish9e26ca82016-02-23 11:43:20 -050032from tempest.lib import exceptions
33
34# redrive rate limited calls at most twice
35MAX_RECURSION_DEPTH = 2
36
37# All the successful HTTP status codes from RFC 7231 & 4918
38HTTP_SUCCESS = (200, 201, 202, 203, 204, 205, 206, 207)
39
40# All the redirection HTTP status codes from RFC 7231 & 4918
41HTTP_REDIRECTION = (300, 301, 302, 303, 304, 305, 306, 307)
42
43# JSON Schema validator and format checker used for JSON Schema validation
ghanshyamf9ded352016-04-12 17:03:01 +090044JSONSCHEMA_VALIDATOR = jsonschema_validator.JSONSCHEMA_VALIDATOR
45FORMAT_CHECKER = jsonschema_validator.FORMAT_CHECKER
Matthew Treinish9e26ca82016-02-23 11:43:20 -050046
47
48class RestClient(object):
49 """Unified OpenStack RestClient class
50
51 This class is used for building openstack api clients on top of. It is
52 intended to provide a base layer for wrapping outgoing http requests in
53 keystone auth as well as providing response code checking and error
54 handling.
55
56 :param auth_provider: an auth provider object used to wrap requests in auth
57 :param str service: The service name to use for the catalog lookup
58 :param str region: The region to use for the catalog lookup
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -060059 :param str name: The endpoint name to use for the catalog lookup; this
60 returns only if the service exists
Matthew Treinish9e26ca82016-02-23 11:43:20 -050061 :param str endpoint_type: The endpoint type to use for the catalog lookup
62 :param int build_interval: Time in seconds between to status checks in
63 wait loops
64 :param int build_timeout: Timeout in seconds to wait for a wait operation.
65 :param bool disable_ssl_certificate_validation: Set to true to disable ssl
66 certificate validation
67 :param str ca_certs: File containing the CA Bundle to use in verifying a
68 TLS server cert
guo yunxian6f24cc42016-07-29 20:03:41 +080069 :param str trace_requests: Regex to use for specifying logging the entirety
Matthew Treinish9e26ca82016-02-23 11:43:20 -050070 of the request and response payload
zhufl071e94c2016-07-12 10:26:34 +080071 :param str http_timeout: Timeout in seconds to wait for the http request to
72 return
Matthew Treinish74514402016-09-01 11:44:57 -040073 :param str proxy_url: http proxy url to use.
Jens Harbott3ffa54e2018-07-04 11:59:49 +000074 :param bool follow_redirects: Set to false to stop following redirects.
Matthew Treinish9e26ca82016-02-23 11:43:20 -050075 """
Matthew Treinish9e26ca82016-02-23 11:43:20 -050076
77 # The version of the API this client implements
78 api_version = None
79
80 LOG = logging.getLogger(__name__)
81
82 def __init__(self, auth_provider, service, region,
83 endpoint_type='publicURL',
84 build_interval=1, build_timeout=60,
85 disable_ssl_certificate_validation=False, ca_certs=None,
Matthew Treinish74514402016-09-01 11:44:57 -040086 trace_requests='', name=None, http_timeout=None,
Jens Harbott3ffa54e2018-07-04 11:59:49 +000087 proxy_url=None, follow_redirects=True):
Matthew Treinish9e26ca82016-02-23 11:43:20 -050088 self.auth_provider = auth_provider
89 self.service = service
90 self.region = region
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -060091 self.name = name
Matthew Treinish9e26ca82016-02-23 11:43:20 -050092 self.endpoint_type = endpoint_type
93 self.build_interval = build_interval
94 self.build_timeout = build_timeout
95 self.trace_requests = trace_requests
96
97 self._skip_path = False
98 self.general_header_lc = set(('cache-control', 'connection',
99 'date', 'pragma', 'trailer',
100 'transfer-encoding', 'via',
101 'warning'))
102 self.response_header_lc = set(('accept-ranges', 'age', 'etag',
103 'location', 'proxy-authenticate',
104 'retry-after', 'server',
105 'vary', 'www-authenticate'))
zhufl8464cef2020-12-11 10:51:25 +0800106 self.dscv = disable_ssl_certificate_validation
Matthew Treinish74514402016-09-01 11:44:57 -0400107
108 if proxy_url:
109 self.http_obj = http.ClosingProxyHttp(
110 proxy_url,
zhufl8464cef2020-12-11 10:51:25 +0800111 disable_ssl_certificate_validation=self.dscv,
112 ca_certs=ca_certs,
Jens Harbott3ffa54e2018-07-04 11:59:49 +0000113 timeout=http_timeout, follow_redirects=follow_redirects)
Matthew Treinish74514402016-09-01 11:44:57 -0400114 else:
115 self.http_obj = http.ClosingHttp(
zhufl8464cef2020-12-11 10:51:25 +0800116 disable_ssl_certificate_validation=self.dscv,
117 ca_certs=ca_certs,
Jens Harbott3ffa54e2018-07-04 11:59:49 +0000118 timeout=http_timeout, follow_redirects=follow_redirects)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500119
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500120 def get_headers(self, accept_type=None, send_type=None):
121 """Return the default headers which will be used with outgoing requests
122
123 :param str accept_type: The media type to use for the Accept header, if
124 one isn't provided the object var TYPE will be
125 used
126 :param str send_type: The media-type to use for the Content-Type
127 header, if one isn't provided the object var
128 TYPE will be used
129 :rtype: dict
130 :return: The dictionary of headers which can be used in the headers
131 dict for outgoing request
132 """
133 if accept_type is None:
Masayuki Igawa189b92f2017-04-24 18:57:17 +0900134 accept_type = 'json'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500135 if send_type is None:
Masayuki Igawa189b92f2017-04-24 18:57:17 +0900136 send_type = 'json'
Ilya Shakhat1291bb42017-11-29 18:08:16 +0100137 headers = {'Content-Type': 'application/%s' % send_type,
138 'Accept': 'application/%s' % accept_type}
139 headers.update(profiler.serialize_as_http_headers())
140 return headers
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500141
142 def __str__(self):
143 STRING_LIMIT = 80
144 str_format = ("service:%s, base_url:%s, "
145 "filters: %s, build_interval:%s, build_timeout:%s"
146 "\ntoken:%s..., \nheaders:%s...")
147 return str_format % (self.service, self.base_url,
148 self.filters, self.build_interval,
149 self.build_timeout,
150 str(self.token)[0:STRING_LIMIT],
151 str(self.get_headers())[0:STRING_LIMIT])
152
153 @property
154 def user(self):
155 """The username used for requests
156
157 :rtype: string
158 :return: The username being used for requests
159 """
160
161 return self.auth_provider.credentials.username
162
163 @property
164 def user_id(self):
165 """The user_id used for requests
166
167 :rtype: string
168 :return: The user id being used for requests
169 """
170 return self.auth_provider.credentials.user_id
171
172 @property
173 def tenant_name(self):
174 """The tenant/project being used for requests
175
176 :rtype: string
177 :return: The tenant/project name being used for requests
178 """
179 return self.auth_provider.credentials.tenant_name
180
181 @property
Rodolfo Alonso Hernandezc1449d42020-02-15 13:24:28 +0000182 def project_id(self):
183 """The project id being used for requests
184
185 :rtype: string
186 :return: The project id being used for requests
187 """
188 return self.auth_provider.credentials.tenant_id
189
190 @property
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500191 def tenant_id(self):
192 """The tenant/project id being used for requests
193
194 :rtype: string
195 :return: The tenant/project id being used for requests
196 """
Rodolfo Alonso Hernandezc1449d42020-02-15 13:24:28 +0000197 # NOTE(ralonsoh): this property should be deprecated, reference
198 # blueprint adopt-oslo-versioned-objects-for-db.
199 versionutils.report_deprecated_feature(
200 self.LOG, '"tenant_id" property is deprecated for removal, use '
201 '"project_id" instead')
202 return self.project_id
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500203
204 @property
205 def password(self):
206 """The password being used for requests
207
208 :rtype: string
209 :return: The password being used for requests
210 """
211 return self.auth_provider.credentials.password
212
213 @property
214 def base_url(self):
215 return self.auth_provider.base_url(filters=self.filters)
216
217 @property
218 def token(self):
219 return self.auth_provider.get_token()
220
221 @property
222 def filters(self):
223 _filters = dict(
224 service=self.service,
225 endpoint_type=self.endpoint_type,
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -0600226 region=self.region,
227 name=self.name
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500228 )
229 if self.api_version is not None:
230 _filters['api_version'] = self.api_version
231 if self._skip_path:
232 _filters['skip_path'] = self._skip_path
233 return _filters
234
235 def skip_path(self):
236 """When set, ignore the path part of the base URL from the catalog"""
237 self._skip_path = True
238
239 def reset_path(self):
240 """When reset, use the base URL from the catalog as-is"""
241 self._skip_path = False
242
243 @classmethod
244 def expected_success(cls, expected_code, read_code):
245 """Check expected success response code against the http response
246
247 :param int expected_code: The response code that is expected.
248 Optionally a list of integers can be used
249 to specify multiple valid success codes
250 :param int read_code: The response code which was returned in the
251 response
252 :raises AssertionError: if the expected_code isn't a valid http success
253 response code
254 :raises exceptions.InvalidHttpSuccessCode: if the read code isn't an
255 expected http success code
256 """
ghanshyamc3074202016-04-18 15:20:45 +0900257 if not isinstance(read_code, int):
258 raise TypeError("'read_code' must be an int instead of (%s)"
259 % type(read_code))
260
Hanxi2f977db2016-09-01 17:31:28 +0800261 assert_msg = ("This function only allowed to use for HTTP status "
262 "codes which explicitly defined in the RFC 7231 & 4918. "
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500263 "{0} is not a defined Success Code!"
264 ).format(expected_code)
265 if isinstance(expected_code, list):
266 for code in expected_code:
267 assert code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg
268 else:
269 assert expected_code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg
270
271 # NOTE(afazekas): the http status code above 400 is processed by
272 # the _error_checker method
273 if read_code < 400:
zhufl4d2f5152017-01-17 11:16:12 +0800274 pattern = ("Unexpected http success status code {0}, "
275 "The expected status code is {1}")
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500276 if ((not isinstance(expected_code, list) and
277 (read_code != expected_code)) or
278 (isinstance(expected_code, list) and
279 (read_code not in expected_code))):
280 details = pattern.format(read_code, expected_code)
281 raise exceptions.InvalidHttpSuccessCode(details)
282
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200283 def post(self, url, body, headers=None, extra_headers=False,
284 chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500285 """Send a HTTP POST request using keystone auth
286
287 :param str url: the relative url to send the post request to
288 :param dict body: the request body
289 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300290 :param bool extra_headers: Boolean value than indicates if the headers
291 returned by the get_headers() method are to
292 be used but additional headers are needed in
293 the request pass them in as a dict.
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200294 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500295 :return: a tuple with the first entry containing the response headers
296 and the second the response body
297 :rtype: tuple
298 """
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200299 return self.request('POST', url, extra_headers, headers, body, chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500300
301 def get(self, url, headers=None, extra_headers=False):
302 """Send a HTTP GET request using keystone service catalog and auth
303
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530304 :param str url: the relative url to send the get request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500305 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300306 :param bool extra_headers: Boolean value than indicates if the headers
307 returned by the get_headers() method are to
308 be used but additional headers are needed in
309 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500310 :return: a tuple with the first entry containing the response headers
311 and the second the response body
312 :rtype: tuple
313 """
314 return self.request('GET', url, extra_headers, headers)
315
316 def delete(self, url, headers=None, body=None, extra_headers=False):
317 """Send a HTTP DELETE request using keystone service catalog and auth
318
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530319 :param str url: the relative url to send the delete request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500320 :param dict headers: The headers to use for the request
321 :param dict body: the request body
vsaienko9eb846b2016-04-09 00:35:47 +0300322 :param bool extra_headers: Boolean value than indicates if the headers
323 returned by the get_headers() method are to
324 be used but additional headers are needed in
325 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500326 :return: a tuple with the first entry containing the response headers
327 and the second the response body
328 :rtype: tuple
329 """
330 return self.request('DELETE', url, extra_headers, headers, body)
331
332 def patch(self, url, body, headers=None, extra_headers=False):
333 """Send a HTTP PATCH request using keystone service catalog and auth
334
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530335 :param str url: the relative url to send the patch request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500336 :param dict body: the request body
337 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300338 :param bool extra_headers: Boolean value than indicates if the headers
339 returned by the get_headers() method are to
340 be used but additional headers are needed in
341 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500342 :return: a tuple with the first entry containing the response headers
343 and the second the response body
344 :rtype: tuple
345 """
346 return self.request('PATCH', url, extra_headers, headers, body)
347
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200348 def put(self, url, body, headers=None, extra_headers=False, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500349 """Send a HTTP PUT request using keystone service catalog and auth
350
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530351 :param str url: the relative url to send the put request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500352 :param dict body: the request body
353 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300354 :param bool extra_headers: Boolean value than indicates if the headers
355 returned by the get_headers() method are to
356 be used but additional headers are needed in
357 the request pass them in as a dict.
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200358 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500359 :return: a tuple with the first entry containing the response headers
360 and the second the response body
361 :rtype: tuple
362 """
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200363 return self.request('PUT', url, extra_headers, headers, body, chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500364
365 def head(self, url, headers=None, extra_headers=False):
366 """Send a HTTP HEAD request using keystone service catalog and auth
367
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530368 :param str url: the relative url to send the head request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500369 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300370 :param bool extra_headers: Boolean value than indicates if the headers
371 returned by the get_headers() method are to
372 be used but additional headers are needed in
373 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500374 :return: a tuple with the first entry containing the response headers
375 and the second the response body
376 :rtype: tuple
377 """
378 return self.request('HEAD', url, extra_headers, headers)
379
380 def copy(self, url, headers=None, extra_headers=False):
381 """Send a HTTP COPY request using keystone service catalog and auth
382
Pallav Gupta1f6cc862019-02-22 01:17:35 +0530383 :param str url: the relative url to send the copy request to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500384 :param dict headers: The headers to use for the request
vsaienko9eb846b2016-04-09 00:35:47 +0300385 :param bool extra_headers: Boolean value than indicates if the headers
386 returned by the get_headers() method are to
387 be used but additional headers are needed in
388 the request pass them in as a dict.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500389 :return: a tuple with the first entry containing the response headers
390 and the second the response body
391 :rtype: tuple
392 """
393 return self.request('COPY', url, extra_headers, headers)
394
395 def get_versions(self):
sunqingliang699690f62018-11-09 15:03:17 +0800396 """Get the versions on an endpoint from the keystone catalog
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500397
398 This method will make a GET request on the baseurl from the keystone
399 catalog to return a list of API versions. It is expected that a GET
400 on the endpoint in the catalog will return a list of supported API
401 versions.
402
junboli872ca872017-07-21 13:24:38 +0800403 :return: tuple with response headers and list of version numbers
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500404 :rtype: tuple
405 """
406 resp, body = self.get('')
407 body = self._parse_resp(body)
408 versions = map(lambda x: x['id'], body)
409 return resp, versions
410
411 def _get_request_id(self, resp):
412 for i in ('x-openstack-request-id', 'x-compute-request-id'):
413 if i in resp:
414 return resp[i]
415 return ""
416
417 def _safe_body(self, body, maxlen=4096):
418 # convert a structure into a string safely
419 try:
likui19b70a32020-12-02 13:25:18 +0800420 text = str(body)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500421 except UnicodeDecodeError:
422 # if this isn't actually text, return marker that
423 return "<BinaryData: removed>"
424 if len(text) > maxlen:
425 return text[:maxlen]
426 else:
427 return text
428
guo yunxian9f749f92016-08-25 10:55:04 +0800429 def _log_request_start(self, method, req_url):
Jordan Pittier9e227c52016-02-09 14:35:18 +0100430 caller_name = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500431 if self.trace_requests and re.search(self.trace_requests, caller_name):
Jordan Pittier525ec712016-12-07 17:51:26 +0100432 self.LOG.debug('Starting Request (%s): %s %s', caller_name,
433 method, req_url)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500434
guo yunxian9f749f92016-08-25 10:55:04 +0800435 def _log_request_full(self, resp, req_headers=None, req_body=None,
436 resp_body=None, extra=None):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500437 if 'X-Auth-Token' in req_headers:
438 req_headers['X-Auth-Token'] = '<omitted>'
Ken'ichi Ohmichi2902a7b2018-07-14 02:31:03 +0000439 if 'X-Subject-Token' in req_headers:
440 req_headers['X-Subject-Token'] = '<omitted>'
Andrea Frittoli (andreaf)a1edb2d2016-05-10 16:09:59 +0100441 # A shallow copy is sufficient
442 resp_log = resp.copy()
443 if 'x-subject-token' in resp_log:
444 resp_log['x-subject-token'] = '<omitted>'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500445 log_fmt = """Request - Headers: %s
446 Body: %s
447 Response - Headers: %s
448 Body: %s"""
449
450 self.LOG.debug(
Jordan Pittier525ec712016-12-07 17:51:26 +0100451 log_fmt,
452 str(req_headers),
453 self._safe_body(req_body),
454 str(resp_log),
455 self._safe_body(resp_body),
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500456 extra=extra)
457
458 def _log_request(self, method, req_url, resp,
459 secs="", req_headers=None,
460 req_body=None, resp_body=None):
461 if req_headers is None:
462 req_headers = {}
463 # if we have the request id, put it in the right part of the log
464 extra = dict(request_id=self._get_request_id(resp))
465 # NOTE(sdague): while we still have 6 callers to this function
466 # we're going to just provide work around on who is actually
467 # providing timings by gracefully adding no content if they don't.
468 # Once we're down to 1 caller, clean this up.
Jordan Pittier9e227c52016-02-09 14:35:18 +0100469 caller_name = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500470 if secs:
471 secs = " %.3fs" % secs
472 self.LOG.info(
Jordan Pittier525ec712016-12-07 17:51:26 +0100473 'Request (%s): %s %s %s%s',
474 caller_name,
475 resp['status'],
476 method,
477 req_url,
478 secs,
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500479 extra=extra)
480
481 # Also look everything at DEBUG if you want to filter this
482 # out, don't run at debug.
Anusha Raminenif3eb9472017-01-13 08:54:01 +0530483 if self.LOG.isEnabledFor(logging.DEBUG):
guo yunxian9f749f92016-08-25 10:55:04 +0800484 self._log_request_full(resp, req_headers, req_body,
485 resp_body, extra)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500486
487 def _parse_resp(self, body):
488 try:
489 body = json.loads(body)
490 except ValueError:
491 return body
492
493 # We assume, that if the first value of the deserialized body's
494 # item set is a dict or a list, that we just return the first value
495 # of deserialized body.
496 # Essentially "cutting out" the first placeholder element in a body
497 # that looks like this:
498 #
499 # {
500 # "users": [
501 # ...
502 # ]
503 # }
504 try:
505 # Ensure there are not more than one top-level keys
506 # NOTE(freerunner): Ensure, that JSON is not nullable to
507 # to prevent StopIteration Exception
Ken'ichi Ohmichi69a8edc2017-04-28 11:41:20 -0700508 if not hasattr(body, "keys") or len(body.keys()) != 1:
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500509 return body
510 # Just return the "wrapped" element
songwenpinge6623072021-02-22 14:47:34 +0800511 _, first_item = tuple(body.items())[0]
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500512 if isinstance(first_item, (dict, list)):
513 return first_item
514 except (ValueError, IndexError):
515 pass
516 return body
517
518 def response_checker(self, method, resp, resp_body):
519 """A sanity check on the response from a HTTP request
520
521 This method does a sanity check on whether the response from an HTTP
522 request conforms the HTTP RFC.
523
524 :param str method: The HTTP verb of the request associated with the
525 response being passed in.
526 :param resp: The response headers
527 :param resp_body: The body of the response
528 :raises ResponseWithNonEmptyBody: If the response with the status code
529 is not supposed to have a body
530 :raises ResponseWithEntity: If the response code is 205 but has an
531 entity
532 """
533 if (resp.status in set((204, 205, 304)) or resp.status < 200 or
534 method.upper() == 'HEAD') and resp_body:
535 raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
536 # NOTE(afazekas):
537 # If the HTTP Status Code is 205
538 # 'The response MUST NOT include an entity.'
539 # A HTTP entity has an entity-body and an 'entity-header'.
540 # In the HTTP response specification (Section 6) the 'entity-header'
541 # 'generic-header' and 'response-header' are in OR relation.
542 # All headers not in the above two group are considered as entity
543 # header in every interpretation.
544
545 if (resp.status == 205 and
546 0 != len(set(resp.keys()) - set(('status',)) -
547 self.response_header_lc - self.general_header_lc)):
Matt Riedemann91d92422019-01-29 16:19:49 -0500548 raise exceptions.ResponseWithEntity()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500549 # NOTE(afazekas)
550 # Now the swift sometimes (delete not empty container)
551 # returns with non json error response, we can create new rest class
552 # for swift.
553 # Usually RFC2616 says error responses SHOULD contain an explanation.
554 # The warning is normal for SHOULD/SHOULD NOT case
555
556 # Likely it will cause an error
557 if method != 'HEAD' and not resp_body and resp.status >= 400:
558 self.LOG.warning("status >= 400 response with empty body")
559
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200560 def _request(self, method, url, headers=None, body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500561 """A simple HTTP request interface."""
562 # Authenticate the request with the auth provider
563 req_url, req_headers, req_body = self.auth_provider.auth_request(
564 method, url, headers, body, self.filters)
565
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500566 resp, resp_body = self.raw_request(
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200567 req_url, method, headers=req_headers, body=req_body,
568 chunked=chunked
569 )
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500570 # Verify HTTP response codes
571 self.response_checker(method, resp, resp_body)
572
573 return resp, resp_body
574
Ghanshyam Mann47a41992019-10-02 16:56:26 +0000575 def raw_request(self, url, method, headers=None, body=None, chunked=False,
576 log_req_body=None):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500577 """Send a raw HTTP request without the keystone catalog or auth
578
579 This method sends a HTTP request in the same manner as the request()
580 method, however it does so without using keystone auth or the catalog
581 to determine the base url. Additionally no response handling is done
582 the results from the request are just returned.
583
584 :param str url: Full url to send the request
585 :param str method: The HTTP verb to use for the request
zhuflcf35f652018-08-17 10:13:43 +0800586 :param dict headers: Headers to use for the request. If none are
587 specified, then the headers returned from the
588 get_headers() method are used. If the request
589 explicitly requires no headers use an empty dict.
Anh Trand44a8be2016-03-25 09:49:14 +0700590 :param str body: Body to send with the request
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200591 :param bool chunked: sends the body with chunked encoding
Ghanshyam Mann47a41992019-10-02 16:56:26 +0000592 :param str log_req_body: Whether to log the request body or not.
593 It is default to None which means request
594 body is safe to log otherwise pass any string
595 you want to log in place of request body.
596 For example: '<omitted>'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500597 :rtype: tuple
598 :return: a tuple with the first entry containing the response headers
599 and the second the response body
600 """
601 if headers is None:
602 headers = self.get_headers()
Ghanshyam Manncb3cf032019-09-26 00:02:54 +0000603 # Do the actual request, and time it
604 start = time.time()
605 self._log_request_start(method, url)
606 resp, resp_body = self.http_obj.request(
607 url, method, headers=headers,
608 body=body, chunked=chunked)
609 end = time.time()
Ghanshyam Mann47a41992019-10-02 16:56:26 +0000610 req_body = body if log_req_body is None else log_req_body
Ghanshyam Manncb3cf032019-09-26 00:02:54 +0000611 self._log_request(method, url, resp, secs=(end - start),
Ghanshyam Mann47a41992019-10-02 16:56:26 +0000612 req_headers=headers, req_body=req_body,
Ghanshyam Manncb3cf032019-09-26 00:02:54 +0000613 resp_body=resp_body)
614 return resp, resp_body
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500615
616 def request(self, method, url, extra_headers=False, headers=None,
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200617 body=None, chunked=False):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500618 """Send a HTTP request with keystone auth and using the catalog
619
620 This method will send an HTTP request using keystone auth in the
621 headers and the catalog to determine the endpoint to use for the
622 baseurl to send the request to. Additionally
623
624 When a response is received it will check it to see if an error
625 response was received. If it was an exception will be raised to enable
626 it to be handled quickly.
627
628 This method will also handle rate-limiting, if a 413 response code is
629 received it will retry the request after waiting the 'retry-after'
630 duration from the header.
631
632 :param str method: The HTTP verb to use for the request
633 :param str url: Relative url to send the request to
vsaienko9eb846b2016-04-09 00:35:47 +0300634 :param bool extra_headers: Boolean value than indicates if the headers
635 returned by the get_headers() method are to
636 be used but additional headers are needed in
637 the request pass them in as a dict.
zhuflcf35f652018-08-17 10:13:43 +0800638 :param dict headers: Headers to use for the request. If none are
639 specified, then the headers returned from the
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500640 get_headers() method are used. If the request
641 explicitly requires no headers use an empty dict.
Anh Trand44a8be2016-03-25 09:49:14 +0700642 :param str body: Body to send with the request
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200643 :param bool chunked: sends the body with chunked encoding
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500644 :rtype: tuple
645 :return: a tuple with the first entry containing the response headers
646 and the second the response body
647 :raises UnexpectedContentType: If the content-type of the response
648 isn't an expect type
649 :raises Unauthorized: If a 401 response code is received
650 :raises Forbidden: If a 403 response code is received
651 :raises NotFound: If a 404 response code is received
652 :raises BadRequest: If a 400 response code is received
653 :raises Gone: If a 410 response code is received
654 :raises Conflict: If a 409 response code is received
Kevin Bentona82bc862017-02-13 01:16:13 -0800655 :raises PreconditionFailed: If a 412 response code is received
zhuflcf35f652018-08-17 10:13:43 +0800656 :raises OverLimit: If a 413 response code is received and retry-after
657 is not in the response body or its retry operation
658 exceeds the limits defined by the server
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500659 :raises RateLimitExceeded: If a 413 response code is received and
zhuflcf35f652018-08-17 10:13:43 +0800660 retry-after is in the response body and
661 its retry operation does not exceeds the
662 limits defined by the server
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500663 :raises InvalidContentType: If a 415 response code is received
664 :raises UnprocessableEntity: If a 422 response code is received
665 :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
666 and couldn't be parsed
667 :raises NotImplemented: If a 501 response code is received
668 :raises ServerFault: If a 500 response code is received
669 :raises UnexpectedResponseCode: If a response code above 400 is
670 received and it doesn't fall into any
671 of the handled checks
672 """
673 # if extra_headers is True
674 # default headers would be added to headers
675 retry = 0
676
677 if headers is None:
678 # NOTE(vponomaryov): if some client do not need headers,
679 # it should explicitly pass empty dict
680 headers = self.get_headers()
681 elif extra_headers:
682 try:
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500683 headers.update(self.get_headers())
684 except (ValueError, TypeError):
685 headers = self.get_headers()
686
Jordan Pittier4408c4a2016-04-29 15:05:09 +0200687 resp, resp_body = self._request(method, url, headers=headers,
688 body=body, chunked=chunked)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500689
690 while (resp.status == 413 and
691 'retry-after' in resp and
692 not self.is_absolute_limit(
693 resp, self._parse_resp(resp_body)) and
694 retry < MAX_RECURSION_DEPTH):
695 retry += 1
Paul Glass119565a2016-04-06 11:41:42 -0500696 delay = self._get_retry_after_delay(resp)
697 self.LOG.debug(
698 "Sleeping %s seconds based on retry-after header", delay
699 )
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500700 time.sleep(delay)
701 resp, resp_body = self._request(method, url,
702 headers=headers, body=body)
Ken'ichi Ohmichi17051e82016-10-20 11:46:06 -0700703 self._error_checker(resp, resp_body)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500704 return resp, resp_body
705
Paul Glass119565a2016-04-06 11:41:42 -0500706 def _get_retry_after_delay(self, resp):
707 """Extract the delay from the retry-after header.
708
709 This supports both integer and HTTP date formatted retry-after headers
710 per RFC 2616.
711
712 :param resp: The response containing the retry-after headers
713 :rtype: int
714 :return: The delay in seconds, clamped to be at least 1 second
715 :raises ValueError: On failing to parse the delay
716 """
717 delay = None
718 try:
719 delay = int(resp['retry-after'])
720 except (ValueError, KeyError):
721 pass
722
723 try:
724 retry_timestamp = self._parse_http_date(resp['retry-after'])
725 date_timestamp = self._parse_http_date(resp['date'])
726 delay = int(retry_timestamp - date_timestamp)
727 except (ValueError, OverflowError, KeyError):
728 pass
729
730 if delay is None:
731 raise ValueError(
732 "Failed to parse retry-after header %r as either int or "
733 "HTTP-date." % resp.get('retry-after')
734 )
735
736 # Retry-after headers do not have sub-second precision. Clients may
737 # receive a delay of 0. After sleeping 0 seconds, we would (likely) hit
738 # another 413. To avoid this, always sleep at least 1 second.
739 return max(1, delay)
740
741 def _parse_http_date(self, val):
742 """Parse an HTTP date, like 'Fri, 31 Dec 1999 23:59:59 GMT'.
743
744 Return an epoch timestamp (float), as returned by time.mktime().
745 """
746 parts = email.utils.parsedate(val)
747 if not parts:
748 raise ValueError("Failed to parse date %s" % val)
749 return time.mktime(parts)
750
Ken'ichi Ohmichi17051e82016-10-20 11:46:06 -0700751 def _error_checker(self, resp, resp_body):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500752
753 # NOTE(mtreinish): Check for httplib response from glance_http. The
754 # object can't be used here because importing httplib breaks httplib2.
755 # If another object from a class not imported were passed here as
756 # resp this could possibly fail
757 if str(type(resp)) == "<type 'instance'>":
758 ctype = resp.getheader('content-type')
759 else:
760 try:
761 ctype = resp['content-type']
762 # NOTE(mtreinish): Keystone delete user responses doesn't have a
763 # content-type header. (They don't have a body) So just pretend it
764 # is set.
765 except KeyError:
766 ctype = 'application/json'
767
768 # It is not an error response
769 if resp.status < 400:
770 return
771
zhipenghd1db0c72017-02-21 04:40:07 -0500772 # NOTE(zhipengh): There is a purposefully duplicate of content-type
773 # with the only difference is with or without spaces, as specified
774 # in RFC7231.
775 JSON_ENC = ['application/json', 'application/json; charset=utf-8',
776 'application/json;charset=utf-8']
777
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500778 # NOTE(mtreinish): This is for compatibility with Glance and swift
779 # APIs. These are the return content types that Glance api v1
780 # (and occasionally swift) are using.
zhipenghd1db0c72017-02-21 04:40:07 -0500781 # NOTE(zhipengh): There is a purposefully duplicate of content-type
782 # with the only difference is with or without spaces, as specified
783 # in RFC7231.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500784 TXT_ENC = ['text/plain', 'text/html', 'text/html; charset=utf-8',
zhipenghd1db0c72017-02-21 04:40:07 -0500785 'text/plain; charset=utf-8', 'text/html;charset=utf-8',
786 'text/plain;charset=utf-8']
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500787
788 if ctype.lower() in JSON_ENC:
789 parse_resp = True
790 elif ctype.lower() in TXT_ENC:
791 parse_resp = False
792 else:
793 raise exceptions.UnexpectedContentType(str(resp.status),
794 resp=resp)
795
796 if resp.status == 401:
797 if parse_resp:
798 resp_body = self._parse_resp(resp_body)
799 raise exceptions.Unauthorized(resp_body, resp=resp)
800
801 if resp.status == 403:
802 if parse_resp:
803 resp_body = self._parse_resp(resp_body)
804 raise exceptions.Forbidden(resp_body, resp=resp)
805
806 if resp.status == 404:
807 if parse_resp:
808 resp_body = self._parse_resp(resp_body)
809 raise exceptions.NotFound(resp_body, resp=resp)
810
811 if resp.status == 400:
812 if parse_resp:
813 resp_body = self._parse_resp(resp_body)
814 raise exceptions.BadRequest(resp_body, resp=resp)
815
816 if resp.status == 410:
817 if parse_resp:
818 resp_body = self._parse_resp(resp_body)
819 raise exceptions.Gone(resp_body, resp=resp)
820
821 if resp.status == 409:
822 if parse_resp:
823 resp_body = self._parse_resp(resp_body)
824 raise exceptions.Conflict(resp_body, resp=resp)
825
Kevin Bentona82bc862017-02-13 01:16:13 -0800826 if resp.status == 412:
827 if parse_resp:
828 resp_body = self._parse_resp(resp_body)
829 raise exceptions.PreconditionFailed(resp_body, resp=resp)
830
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500831 if resp.status == 413:
832 if parse_resp:
833 resp_body = self._parse_resp(resp_body)
834 if self.is_absolute_limit(resp, resp_body):
835 raise exceptions.OverLimit(resp_body, resp=resp)
836 else:
837 raise exceptions.RateLimitExceeded(resp_body, resp=resp)
838
839 if resp.status == 415:
840 if parse_resp:
841 resp_body = self._parse_resp(resp_body)
842 raise exceptions.InvalidContentType(resp_body, resp=resp)
843
844 if resp.status == 422:
845 if parse_resp:
846 resp_body = self._parse_resp(resp_body)
847 raise exceptions.UnprocessableEntity(resp_body, resp=resp)
848
849 if resp.status in (500, 501):
850 message = resp_body
851 if parse_resp:
852 try:
853 resp_body = self._parse_resp(resp_body)
854 except ValueError:
855 # If response body is a non-json string message.
856 # Use resp_body as is and raise InvalidResponseBody
857 # exception.
858 raise exceptions.InvalidHTTPResponseBody(message)
859 else:
860 if isinstance(resp_body, dict):
861 # I'm seeing both computeFault
862 # and cloudServersFault come back.
863 # Will file a bug to fix, but leave as is for now.
864 if 'cloudServersFault' in resp_body:
865 message = resp_body['cloudServersFault']['message']
866 elif 'computeFault' in resp_body:
867 message = resp_body['computeFault']['message']
868 elif 'error' in resp_body:
869 message = resp_body['error']['message']
870 elif 'message' in resp_body:
871 message = resp_body['message']
872 else:
873 message = resp_body
874
875 if resp.status == 501:
876 raise exceptions.NotImplemented(resp_body, resp=resp,
877 message=message)
878 else:
879 raise exceptions.ServerFault(resp_body, resp=resp,
880 message=message)
881
882 if resp.status >= 400:
883 raise exceptions.UnexpectedResponseCode(str(resp.status),
884 resp=resp)
885
886 def is_absolute_limit(self, resp, resp_body):
Takashi Kajinami6354f612021-07-17 00:37:34 +0900887 if (not isinstance(resp_body, abc.Mapping) or
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500888 'retry-after' not in resp):
889 return True
Paul Glass119565a2016-04-06 11:41:42 -0500890 return 'exceed' in resp_body.get('message', 'blabla')
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500891
Benny Kopilovf5e277c2021-02-08 12:27:37 +0200892 def wait_for_resource_deletion(self, id, *args, **kwargs):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500893 """Waits for a resource to be deleted
894
895 This method will loop over is_resource_deleted until either
896 is_resource_deleted returns True or the build timeout is reached. This
897 depends on is_resource_deleted being implemented
898
899 :param str id: The id of the resource to check
900 :raises TimeoutException: If the build_timeout has elapsed and the
901 resource still hasn't been deleted
902 """
903 start_time = int(time.time())
904 while True:
Benny Kopilovf5e277c2021-02-08 12:27:37 +0200905 if self.is_resource_deleted(id, *args, **kwargs):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500906 return
907 if int(time.time()) - start_time >= self.build_timeout:
908 message = ('Failed to delete %(resource_type)s %(id)s within '
Sampat Ponnagantief552162021-03-17 18:07:36 +0000909 'the required time (%(timeout)s s). Timer started '
Eric Harney941043f2022-01-07 14:21:50 -0500910 'at %(start_time)s. Timer ended at %(end_time)s. '
911 'Waited for %(wait_time)s s.' %
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500912 {'resource_type': self.resource_type, 'id': id,
Sampat Ponnagantief552162021-03-17 18:07:36 +0000913 'timeout': self.build_timeout,
914 'start_time': start_time,
915 'end_time': int(time.time()),
916 'wait_time': int(time.time()) - start_time})
Jordan Pittier9e227c52016-02-09 14:35:18 +0100917 caller = test_utils.find_test_caller()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500918 if caller:
919 message = '(%s) %s' % (caller, message)
920 raise exceptions.TimeoutException(message)
921 time.sleep(self.build_interval)
922
Abhishek Kekane7cff1302020-07-16 10:30:13 +0000923 def wait_for_resource_activation(self, id):
924 """Waits for a resource to become active
925
926 This method will loop over is_resource_active until either
927 is_resource_active returns True or the build timeout is reached. This
928 depends on is_resource_active being implemented
929
930 :param str id: The id of the resource to check
931 :raises TimeoutException: If the build_timeout has elapsed and the
932 resource still hasn't been active
933 """
934 start_time = int(time.time())
935 while True:
936 if self.is_resource_active(id):
937 return
938 if int(time.time()) - start_time >= self.build_timeout:
939 message = ('Failed to reach active state %(resource_type)s '
940 '%(id)s within the required time (%(timeout)s s).' %
941 {'resource_type': self.resource_type, 'id': id,
942 'timeout': self.build_timeout})
943 caller = test_utils.find_test_caller()
944 if caller:
945 message = '(%s) %s' % (caller, message)
946 raise exceptions.TimeoutException(message)
947 time.sleep(self.build_interval)
948
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500949 def is_resource_deleted(self, id):
950 """Subclasses override with specific deletion detection."""
951 message = ('"%s" does not implement is_resource_deleted'
952 % self.__class__.__name__)
953 raise NotImplementedError(message)
954
Abhishek Kekane7cff1302020-07-16 10:30:13 +0000955 def is_resource_active(self, id):
956 """Subclasses override with specific active detection."""
957 message = ('"%s" does not implement is_resource_active'
958 % self.__class__.__name__)
959 raise NotImplementedError(message)
960
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500961 @property
962 def resource_type(self):
963 """Returns the primary type of resource this client works with."""
964 return 'resource'
965
966 @classmethod
967 def validate_response(cls, schema, resp, body):
968 # Only check the response if the status code is a success code
969 # TODO(cyeoh): Eventually we should be able to verify that a failure
970 # code if it exists is something that we expect. This is explicitly
971 # declared in the V3 API and so we should be able to export this in
972 # the response schema. For now we'll ignore it.
973 if resp.status in HTTP_SUCCESS + HTTP_REDIRECTION:
974 cls.expected_success(schema['status_code'], resp.status)
975
976 # Check the body of a response
977 body_schema = schema.get('response_body')
978 if body_schema:
979 try:
980 jsonschema.validate(body, body_schema,
981 cls=JSONSCHEMA_VALIDATOR,
982 format_checker=FORMAT_CHECKER)
983 except jsonschema.ValidationError as ex:
guo yunxiana3f55282016-08-10 14:35:16 +0800984 msg = ("HTTP response body is invalid (%s)" % ex)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500985 raise exceptions.InvalidHTTPResponseBody(msg)
986 else:
987 if body:
guo yunxiana3f55282016-08-10 14:35:16 +0800988 msg = ("HTTP response body should not exist (%s)" % body)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500989 raise exceptions.InvalidHTTPResponseBody(msg)
990
991 # Check the header of a response
992 header_schema = schema.get('response_header')
993 if header_schema:
994 try:
995 jsonschema.validate(resp, header_schema,
996 cls=JSONSCHEMA_VALIDATOR,
997 format_checker=FORMAT_CHECKER)
998 except jsonschema.ValidationError as ex:
guo yunxiana3f55282016-08-10 14:35:16 +0800999 msg = ("HTTP response header is invalid (%s)" % ex)
Matthew Treinish9e26ca82016-02-23 11:43:20 -05001000 raise exceptions.InvalidHTTPResponseHeader(msg)
1001
Ken'ichi Ohmichida26b162017-03-03 15:53:46 -08001002 def _get_base_version_url(self):
1003 # TODO(oomichi): This method can be used for auth's replace_version().
1004 # So it is nice to have common logic for the maintenance.
1005 endpoint = self.base_url
1006 url = urllib.parse.urlsplit(endpoint)
1007 new_path = re.split(r'(^|/)+v\d+(\.\d+)?', url.path)[0]
1008 url = list(url)
1009 url[2] = new_path + '/'
1010 return urllib.parse.urlunsplit(url)
1011
Matthew Treinish9e26ca82016-02-23 11:43:20 -05001012
1013class ResponseBody(dict):
1014 """Class that wraps an http response and dict body into a single value.
1015
1016 Callers that receive this object will normally use it as a dict but
1017 can extract the response if needed.
1018 """
1019
1020 def __init__(self, response, body=None):
1021 body_data = body or {}
1022 self.update(body_data)
1023 self.response = response
1024
1025 def __str__(self):
1026 body = super(ResponseBody, self).__str__()
1027 return "response: %s\nBody: %s" % (self.response, body)
1028
1029
1030class ResponseBodyData(object):
1031 """Class that wraps an http response and string data into a single value.
1032
1033 """
1034
1035 def __init__(self, response, data):
1036 self.response = response
1037 self.data = data
1038
1039 def __str__(self):
1040 return "response: %s\nBody: %s" % (self.response, self.data)
1041
1042
1043class ResponseBodyList(list):
1044 """Class that wraps an http response and list body into a single value.
1045
1046 Callers that receive this object will normally use it as a list but
1047 can extract the response if needed.
1048 """
1049
1050 def __init__(self, response, body=None):
1051 body_data = body or []
1052 self.extend(body_data)
1053 self.response = response
1054
1055 def __str__(self):
1056 body = super(ResponseBodyList, self).__str__()
1057 return "response: %s\nBody: %s" % (self.response, body)