Add http process function

This file include all http method process function.

Dependency: I26e6229b5da9746598cbaec7eb37b04be0dc6e21
Change-Id: I20beb71a7a5f648d7b96a956e15b9807e6b1c91d
This commit is contained in:
jinxingfang 2017-03-08 18:16:02 +08:00
parent 5ad394f49d
commit 7f68e7ab85
6 changed files with 367 additions and 0 deletions

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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):

View File

@ -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)