Add http process function
This file include all http method process function. Dependency: I26e6229b5da9746598cbaec7eb37b04be0dc6e21 Change-Id: I20beb71a7a5f648d7b96a956e15b9807e6b1c91d
This commit is contained in:
parent
5ad394f49d
commit
7f68e7ab85
|
@ -5,3 +5,4 @@
|
|||
pbr>=2.0.0 # Apache-2.0
|
||||
oslo.utils>=3.20.0 # Apache-2.0
|
||||
oslo.i18n>=2.1.0 # Apache-2.0
|
||||
oslo.serialization>=1.10.0 # Apache-2.0
|
||||
|
|
|
@ -14,6 +14,7 @@ testscenarios>=0.4 # Apache-2.0/BSD
|
|||
testtools>=1.4.0 # MIT
|
||||
oslo.utils>=3.20.0 # Apache-2.0
|
||||
oslo.i18n>=2.1.0 # Apache-2.0
|
||||
oslo.serialization>=1.10.0 # Apache-2.0
|
||||
|
||||
# releasenotes
|
||||
reno>=1.8.0 # Apache-2.0
|
||||
|
|
|
@ -32,6 +32,11 @@ class ClientException(Exception):
|
|||
message = _("ClientException")
|
||||
|
||||
|
||||
class ValidationError(ClientException):
|
||||
"""Error in validation on API client side."""
|
||||
pass
|
||||
|
||||
|
||||
class HttpError(ClientException):
|
||||
"""The base exception class of all HTTP exceptions"""
|
||||
|
||||
|
@ -85,6 +90,16 @@ class HttpServerError(HttpError):
|
|||
message = _("HTTP Server Error")
|
||||
|
||||
|
||||
class InternalServerError(HttpServerError):
|
||||
"""HTTP 500 - Internal Server Error.
|
||||
|
||||
A generic error message, given when no more specific message is suitable.
|
||||
"""
|
||||
|
||||
http_status = http_client.INTERNAL_SERVER_ERROR
|
||||
message = _("Internal Server Error")
|
||||
|
||||
|
||||
# _code_map cotains all the classes that have http_status attribute
|
||||
_code_map = dict((getattr(obj, 'http_status', None), obj)
|
||||
for name, obj in vars(sys.modules[__name__]).items()
|
||||
|
|
|
@ -0,0 +1,187 @@
|
|||
# Copyright 2017 99cloud, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
import copy
|
||||
import functools
|
||||
import logging
|
||||
import requests
|
||||
import time
|
||||
|
||||
import six
|
||||
from six.moves import http_client
|
||||
import six.moves.urllib.parse as urlparse
|
||||
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import strutils
|
||||
|
||||
from valenceclient.common.i18n import _
|
||||
from valenceclient.common.i18n import _LE
|
||||
from valenceclient import exc
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
USER_AGENT = 'python-valenceclient'
|
||||
|
||||
API_VERSION = '/v1'
|
||||
DEFAULT_VERSION = 1
|
||||
|
||||
DEFAULT_MAX_RETRIES = 5
|
||||
DEFAULT_RETRY_INTERVAL = 2
|
||||
|
||||
|
||||
def _trim_endpoint_api_version(url):
|
||||
"""Trim API version and trailing slash from endpoint."""
|
||||
|
||||
return url.rstrip('/').rstip(API_VERSION)
|
||||
|
||||
|
||||
def _extract_error_json(body):
|
||||
error_json = {}
|
||||
try:
|
||||
body_json = jsonutils.loads(body)
|
||||
if 'error_message' in body_json:
|
||||
raw_msg = body_json['error_message']
|
||||
error_json = jsonutils.loads(raw_msg)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return error_json
|
||||
|
||||
|
||||
def with_retries(func):
|
||||
"""Wrapper for _http_request adding support for retries."""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, url, method, **kwargs):
|
||||
if self.conflict_max_retries is None:
|
||||
self.conflict_max_retries = DEFAULT_MAX_RETRIES
|
||||
if self.conflict_retry_interval is None:
|
||||
self.conflict_retry_interval = DEFAULT_RETRY_INTERVAL
|
||||
|
||||
num_attempts = self.conflict_max_retries + 1
|
||||
for attempt in range(1, num_attempts + 1):
|
||||
try:
|
||||
return func(url, method, **kwargs)
|
||||
except Exception as error:
|
||||
msg = (_LE("Error contacting Valence server: %(error)s."
|
||||
"Attempt %(attempt)d of %(total)d") %
|
||||
{'attempt': attempt,
|
||||
'total': num_attempts,
|
||||
'error': error})
|
||||
if attempt == num_attempts:
|
||||
LOG.error(msg)
|
||||
else:
|
||||
LOG.debug(msg)
|
||||
time.sleep(self.conflict_retry_interval)
|
||||
return wrapper
|
||||
|
||||
|
||||
class HTTPClient(object):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.valence_api_version = kwargs.get('valence_api_version',
|
||||
DEFAULT_VERSION)
|
||||
self.valence_url = kwargs.get('valence_url')
|
||||
self.session = requests.Session()
|
||||
|
||||
def log_curl_request(self, method, url, kwargs):
|
||||
curl = ['curl -i -X %s' % method]
|
||||
|
||||
for (key, value) in kwargs['headers'].items():
|
||||
header = "-H '%s: %s'" % (key, value)
|
||||
curl.append(header)
|
||||
|
||||
if 'body' in kwargs:
|
||||
body = strutils.mask_password(kwargs['body'])
|
||||
curl.append("-d '%s'" % body)
|
||||
|
||||
curl.append(urlparse.urljoin('v1', self.valence_url))
|
||||
LOG.debug(" ".join(curl))
|
||||
|
||||
@staticmethod
|
||||
def log_http_response(resp, body=None):
|
||||
status = (resp.raw.version / 10.0, resp.status_code, resp.reason)
|
||||
dump = ['\nHTTP/%.1f %s %s' % status]
|
||||
dump.extend(['%s: %s' % (k, v) for k, v in resp.headers.items()])
|
||||
dump.append('')
|
||||
if body:
|
||||
body = strutils.mask_password(body)
|
||||
dump.extend([body, ''])
|
||||
LOG.debug('\n'.join(dump))
|
||||
|
||||
def _make_connection_url(self, url):
|
||||
return urlparse.urljoin(self.valence_url, url)
|
||||
|
||||
def _http_request(self, url, method, **kwargs):
|
||||
"""Send an http request with the specified characteristics
|
||||
|
||||
Wrapper around request.Session.request to handle tasks such as
|
||||
setting headers and error handling.
|
||||
"""
|
||||
|
||||
kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
|
||||
kwargs['headers'].setdefault('User-agent', USER_AGENT)
|
||||
|
||||
self.log_curl_request(method, url, kwargs)
|
||||
body = kwargs.pop('body', None)
|
||||
if body:
|
||||
kwargs['data'] = body
|
||||
conn_url = self._make_connection_url(url)
|
||||
try:
|
||||
resp = self.session.request(method, conn_url, **kwargs)
|
||||
except requests.exceptions.RequestException as e:
|
||||
msg = (_("Error has occured while handling request for "
|
||||
"%(url)s: %(e)s") % dict(url=conn_url, e=e))
|
||||
if isinstance(e, ValueError):
|
||||
raise exc.ValidationError(msg)
|
||||
raise exc.ConnectionRefuse(msg)
|
||||
|
||||
self.log_http_response(resp, resp.text)
|
||||
body_iter = six.StringIO(resp.text)
|
||||
|
||||
if resp.status_code >= http_client.BAD_REQUEST:
|
||||
error_json = _extract_error_json(resp.text)
|
||||
raise exc.from_response(resp, error_json.get('faultstring'),
|
||||
error_json.get('debugfino'), method, url)
|
||||
elif resp.status_code in (http_client.FOUND,
|
||||
http_client.USE_PROXY):
|
||||
return self._http_request(resp['location'], method, **kwargs)
|
||||
|
||||
return resp, body_iter
|
||||
|
||||
def json_request(self, method, url, **kwargs):
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers'].setdefault('Content-Type', 'application/json')
|
||||
kwargs['headers'].setdefault('Accept', 'application/json')
|
||||
if 'body' in kwargs:
|
||||
kwargs['body'] = jsonutils.dump_as_bytes(kwargs['body'])
|
||||
|
||||
resp, body_iter = self._http_request(url, method, **kwargs)
|
||||
content_type = resp.headers.get('Content-Type')
|
||||
if(resp.status_code in (http_client.NO_CONTENT,
|
||||
http_client.RESET_CONTENT)
|
||||
or content_type is None):
|
||||
return resp, list()
|
||||
|
||||
if 'application/json' in content_type:
|
||||
body = ''.join([chunk for chunk in body_iter])
|
||||
try:
|
||||
body = jsonutils.loads(body)
|
||||
except ValueError:
|
||||
LOG.error(_LE('Could not decode response body as JSON'))
|
||||
else:
|
||||
body = None
|
||||
return resp, body
|
|
@ -14,7 +14,15 @@
|
|||
# under the License.
|
||||
|
||||
from valenceclient.common.apiclient import exceptions
|
||||
from valenceclient.common.apiclient.exceptions import BadRequest
|
||||
from valenceclient.common.apiclient.exceptions import ClientException
|
||||
from valenceclient.common.apiclient.exceptions import InternalServerError
|
||||
from valenceclient.common.apiclient.exceptions import ValidationError
|
||||
|
||||
BadRequest = BadRequest
|
||||
ClientException = ClientException
|
||||
InternalServerError = InternalServerError
|
||||
ValidationError = ValidationError
|
||||
|
||||
|
||||
class InvalidValenceUrl(ClientException):
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
# Copyright 2017 99cloud, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
import mock
|
||||
from oslo_serialization import jsonutils
|
||||
|
||||
from six.moves import http_client
|
||||
from valenceclient.common import http
|
||||
from valenceclient import exc
|
||||
from valenceclient.tests.unit import utils
|
||||
|
||||
DEFAULT_TIMEOUT = 600
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_POST = '1234'
|
||||
|
||||
|
||||
def _get_error_body(faultstring=None, debuginfo=None, description=None):
|
||||
if description:
|
||||
error_body = {'description': description}
|
||||
else:
|
||||
error_body = {
|
||||
'faultstring': faultstring,
|
||||
'debuginfo': debuginfo
|
||||
}
|
||||
raw_error_body = jsonutils.dump_as_bytes(error_body)
|
||||
body = {'error_message': raw_error_body}
|
||||
return jsonutils.dumps(body)
|
||||
|
||||
|
||||
class HttpClientTest(utils.BaseTestCase):
|
||||
|
||||
def test_url_generation_trailing_slash_in_base(self):
|
||||
kwargs = {"valence_url": "http://localhost/"}
|
||||
client = http.HTTPClient(**kwargs)
|
||||
url = client._make_connection_url('/redfish/v1')
|
||||
self.assertEqual('http://localhost/redfish/v1', url)
|
||||
|
||||
def test_url_generation_without_trailing_slash_in_base(self):
|
||||
kwargs = {"valence_url": "http://localhost"}
|
||||
client = http.HTTPClient(**kwargs)
|
||||
url = client._make_connection_url('/redfish/v1')
|
||||
self.assertEqual('http://localhost/redfish/v1', url)
|
||||
|
||||
def test_url_generation_without_prefix_slash_in_path(self):
|
||||
kwargs = {"valence_url": "http://localhost"}
|
||||
client = http.HTTPClient(**kwargs)
|
||||
url = client._make_connection_url('redfish/v1')
|
||||
self.assertEqual('http://localhost/redfish/v1', url)
|
||||
|
||||
def test_server_exception_empty_body(self):
|
||||
error_body = _get_error_body()
|
||||
kwargs = {"valence_url": "http://localhost"}
|
||||
client = http.HTTPClient(**kwargs)
|
||||
client.session = utils.mockSession(
|
||||
{'Content-Type': 'application/json'},
|
||||
error_body,
|
||||
version=1,
|
||||
status_code=http_client.INTERNAL_SERVER_ERROR)
|
||||
|
||||
self.assertRaises(exc.InternalServerError,
|
||||
client.json_request,
|
||||
'GET', 'redfish/v1')
|
||||
|
||||
def test_server_exception_msg_only(self):
|
||||
error_body = "test error msg"
|
||||
kwargs = {"valence_url": "http://localhost"}
|
||||
client = http.HTTPClient(**kwargs)
|
||||
client.session = utils.mockSession(
|
||||
{'Content-Type': 'application/json'},
|
||||
error_body,
|
||||
version=1,
|
||||
status_code=http_client.INTERNAL_SERVER_ERROR)
|
||||
|
||||
self.assertRaises(exc.InternalServerError,
|
||||
client.json_request,
|
||||
'GET', 'redfish/v1')
|
||||
|
||||
def test_server_exception_description_only(self):
|
||||
error_msg = "test error msg"
|
||||
error_body = _get_error_body(description=error_msg)
|
||||
kwargs = {"valence_url": "http://localhost/"}
|
||||
client = http.HTTPClient(**kwargs)
|
||||
client.session = utils.mockSession(
|
||||
{'Content-Type': 'application/json'},
|
||||
error_body,
|
||||
version=1,
|
||||
status_code=http_client.BAD_REQUEST)
|
||||
|
||||
self.assertRaises(exc.BadRequest,
|
||||
client.json_request,
|
||||
'GET', 'redfish/v1')
|
||||
|
||||
def test_server_https_request_ok(self):
|
||||
kwargs = {"valence_url": "http://localhost/"}
|
||||
client = http.HTTPClient(**kwargs)
|
||||
client.session = utils.mockSession(
|
||||
{'Content-Type': 'application/json'},
|
||||
'Body',
|
||||
version=1,
|
||||
status_code=http_client.OK)
|
||||
|
||||
client.json_request('GET', 'redfish/v1')
|
||||
|
||||
def test_server_http_not_valide_request(self):
|
||||
kwargs = {"valence_url": "http://localhost/"}
|
||||
client = http.HTTPClient(**kwargs)
|
||||
client.session.request = mock.Mock(
|
||||
side_effect=http.requests.exceptions.InvalidSchema)
|
||||
self.assertRaises(exc.ValidationError, client._http_request, 'GET',
|
||||
'http://localhost/')
|
||||
|
||||
@mock.patch.object(http.LOG, 'debug', autospec=True)
|
||||
def test_log_curl_request_with_body_and_header(self, mock_log):
|
||||
kwargs = {"valence_url": "http://localhost"}
|
||||
client = http.HTTPClient(**kwargs)
|
||||
headers = {'header1': 'value1'}
|
||||
body = 'example body'
|
||||
|
||||
client.log_curl_request('GET', '/redfish/v1/Nodes',
|
||||
{'headers': headers, 'body': body})
|
||||
self.assertTrue(mock_log.called)
|
||||
self.assertTrue(mock_log.call_args[0])
|
||||
self.assertEqual("curl -i -X GET -H 'header1: value1'"
|
||||
" -d 'example body' http://localhost",
|
||||
mock_log.call_args[0][0])
|
||||
|
||||
def test_http_request_client_success(self):
|
||||
kwargs = {"valence_url": "http://localhost/"}
|
||||
client = http.HTTPClient(**kwargs)
|
||||
resp = utils.mockSessionResponse(
|
||||
{'content-type': 'test/plain'},
|
||||
'Body',
|
||||
version=1,
|
||||
status_code=http_client.OK)
|
||||
|
||||
with mock.patch.object(client, 'session',
|
||||
autospec=True) as mock_session:
|
||||
mock_session.request.side_effect = iter([resp])
|
||||
response, body_iter = client._http_request('/redfish/v1/Nodes',
|
||||
'GET')
|
||||
|
||||
self.assertEqual(http_client.OK, response.status_code)
|
Loading…
Reference in New Issue