bp/api-error-codes

Restructured API error codes for Quantum API v1.1
This changeset provides the following changes:
- Only standard HTTP errors for Quantum API v1.1
- Customized fault response body formatting according to API version
- Changes to unit tests to deal with version specific status codes

NOTE: Client side changes are not in this branch.
They should be implemented within bp/quantum-client-1.1.
NOTE-2: Fixing references to QuantumHTTPErrors in extensions framework

Change-Id: I56f50b5c59d73fd6f02905106f0c2a3c8341a741
This commit is contained in:
Salvatore Orlando 2012-01-17 01:23:00 +00:00
parent 640100604b
commit 37956cdde3
7 changed files with 202 additions and 43 deletions

View File

@ -59,6 +59,8 @@ def create_resource(version, controller_dict):
metadata = controller_dict[version][1]
# and the third element the xml namespace
xmlns = controller_dict[version][2]
# and also the function for building the fault body
fault_body_function = faults.fault_body_function(version)
headers_serializer = HeaderSerializer()
xml_serializer = wsgi.XMLDictSerializer(metadata, xmlns)
@ -79,21 +81,35 @@ def create_resource(version, controller_dict):
serializer = wsgi.ResponseSerializer(body_serializers, headers_serializer)
deserializer = wsgi.RequestDeserializer(body_deserializers)
return wsgi.Resource(controller, deserializer, serializer)
return wsgi.Resource(controller,
fault_body_function,
deserializer,
serializer)
def APIFaultWrapper(errors=None):
quantum_error_dict = {
'1.0': faults.Quantum10HTTPError,
'1.1': faults.Quantum11HTTPError
}
def wrapper(func, **kwargs):
def the_func(*args, **kwargs):
try:
# Grab API version from type of controller
controller = args[0]
version = controller.version
return func(*args, **kwargs)
except Exception as e:
if errors is not None and type(e) in errors:
raise faults.QuantumHTTPError(e)
# Version-specific behaviour
quantum_error_class = quantum_error_dict[version]
raise quantum_error_class(e)
# otherwise just re-raise
raise
the_func.__name__ = func.__name__
return the_func

View File

@ -29,7 +29,60 @@ _ALREADYATTACHED_EXPL = 'The resource is already attached to another port'
_NOTIMPLEMENTED_EXPL = 'Not implemented'
class QuantumHTTPError(webob.exc.HTTPClientError):
def fault_body_function_v10(wrapped_exc):
""" This function creates the contents of the body for a fault
response for Quantum API v1.0.
:param wrapped_exc: Exception thrown by the Quantum service
:type wrapped_exc: quantum.common.exceptions.QuantumException
:returns: response body contents and serialization metadata
:rtype: tuple
"""
code = wrapped_exc.status_int
fault_name = hasattr(wrapped_exc, 'title') and \
wrapped_exc.title or "quantumServiceFault"
fault_data = {
fault_name: {
'code': code,
'message': wrapped_exc.explanation,
'detail': str(wrapped_exc.detail)}}
metadata = {'attributes': {fault_name: ['code']}}
return fault_data, metadata
def fault_body_function_v11(wrapped_exc):
""" This function creates the contents of the body for a fault
response for Quantum API v1.0.
:param wrapped_exc: Exception thrown by the Quantum service
:type wrapped_exc: quantum.common.exceptions.QuantumException
:returns: response body contents and serialization metadata
:rtype: tuple
"""
fault_name = hasattr(wrapped_exc, 'type') and \
wrapped_exc.type or "QuantumServiceFault"
# Ensure first letter is capital
fault_name = fault_name[0].upper() + fault_name[1:]
fault_data = {
'QuantumError': {
'type': fault_name,
'message': wrapped_exc.explanation,
'detail': str(wrapped_exc.detail)}}
# Metadata not required for v11
return fault_data, None
def fault_body_function(version):
# dict mapping API version to functions for building the
# fault response body
fault_body_function_dict = {
'1.0': fault_body_function_v10,
'1.1': fault_body_function_v11
}
return fault_body_function_dict.get(version, None)
class Quantum10HTTPError(webob.exc.HTTPClientError):
_fault_dict = {
exceptions.NetworkNotFound: {
@ -76,3 +129,54 @@ class QuantumHTTPError(webob.exc.HTTPClientError):
self.title = _fault_data['title']
self.explanation = _fault_data['explanation']
super(webob.exc.HTTPClientError, self).__init__(inner_exc)
class Quantum11HTTPError(webob.exc.HTTPClientError):
_fault_dict = {
exceptions.NetworkNotFound: {
'code': webob.exc.HTTPNotFound.code,
'title': webob.exc.HTTPNotFound.title,
'type': 'NetworkNotFound',
'explanation': _NETNOTFOUND_EXPL
},
exceptions.NetworkInUse: {
'code': webob.exc.HTTPConflict.code,
'title': webob.exc.HTTPConflict.title,
'type': 'NetworkInUse',
'explanation': _NETINUSE_EXPL
},
exceptions.PortNotFound: {
'code': webob.exc.HTTPNotFound.code,
'title': webob.exc.HTTPNotFound.title,
'type': 'PortNotFound',
'explanation': _PORTNOTFOUND_EXPL
},
exceptions.StateInvalid: {
'code': webob.exc.HTTPBadRequest.code,
'title': webob.exc.HTTPBadRequest.title,
'type': 'RequestedStateInvalid',
'explanation': _STATEINVALID_EXPL
},
exceptions.PortInUse: {
'code': webob.exc.HTTPConflict.code,
'title': webob.exc.HTTPConflict.title,
'type': 'PortInUse',
'explanation': _PORTINUSE_EXPL
},
exceptions.AlreadyAttached: {
'code': webob.exc.HTTPConflict.code,
'title': webob.exc.HTTPConflict.title,
'type': 'AlreadyAttached',
'explanation': _ALREADYATTACHED_EXPL
}
}
def __init__(self, inner_exc):
_fault_data = self._fault_dict.get(type(inner_exc), None)
if _fault_data:
self.code = _fault_data['code']
self.title = _fault_data['title']
self.explanation = _fault_data['explanation']
self.type = _fault_data['type']
super(webob.exc.HTTPClientError, self).__init__(inner_exc)

View File

@ -76,16 +76,13 @@ class Controller(common.QuantumController):
""" Returns a list of network ids """
return self._items(request, tenant_id)
@common.APIFaultWrapper()
@common.APIFaultWrapper([exception.NetworkNotFound])
def show(self, request, tenant_id, id):
""" Returns network details for the given network id """
try:
return self._item(request, tenant_id, id,
net_details=True, port_details=False)
except exception.NetworkNotFound as e:
raise faults.QuantumHTTPError(e)
return self._item(request, tenant_id, id,
net_details=True, port_details=False)
@common.APIFaultWrapper()
@common.APIFaultWrapper([exception.NetworkNotFound])
def detail(self, request, **kwargs):
tenant_id = kwargs.get('tenant_id')
network_id = kwargs.get('id')

View File

@ -184,7 +184,8 @@ class AbstractAPITest(unittest.TestCase):
"A_BAD_ID",
fmt)
show_network_res = show_network_req.get_response(self.api)
self.assertEqual(show_network_res.status_int, 420)
self.assertEqual(show_network_res.status_int,
self._network_not_found_code)
LOG.debug("_test_show_network_not_found - fmt:%s - END", fmt)
def _test_rename_network(self, fmt):
@ -232,7 +233,8 @@ class AbstractAPITest(unittest.TestCase):
new_name,
fmt)
update_network_res = update_network_req.get_response(self.api)
self.assertEqual(update_network_res.status_int, 420)
self.assertEqual(update_network_res.status_int,
self._network_not_found_code)
LOG.debug("_test_rename_network_not_found - fmt:%s - END",
fmt)
@ -279,7 +281,8 @@ class AbstractAPITest(unittest.TestCase):
network_id,
fmt)
delete_network_res = delete_network_req.get_response(self.api)
self.assertEqual(delete_network_res.status_int, 421)
self.assertEqual(delete_network_res.status_int,
self._network_in_use_code)
LOG.debug("_test_delete_network_in_use - fmt:%s - END", fmt)
def _test_delete_network_with_unattached_port(self, fmt):
@ -324,7 +327,8 @@ class AbstractAPITest(unittest.TestCase):
list_port_req = testlib.port_list_request(self.tenant_id,
"A_BAD_ID", fmt)
list_port_res = list_port_req.get_response(self.api)
self.assertEqual(list_port_res.status_int, 420)
self.assertEqual(list_port_res.status_int,
self._network_not_found_code)
LOG.debug("_test_list_ports_networknotfound - fmt:%s - END", fmt)
def _test_list_ports_detail(self, fmt):
@ -413,7 +417,8 @@ class AbstractAPITest(unittest.TestCase):
"A_BAD_ID", port_id,
fmt)
show_port_res = show_port_req.get_response(self.api)
self.assertEqual(show_port_res.status_int, 420)
self.assertEqual(show_port_res.status_int,
self._network_not_found_code)
LOG.debug("_test_show_port_networknotfound - fmt:%s - END",
fmt)
@ -425,7 +430,8 @@ class AbstractAPITest(unittest.TestCase):
"A_BAD_ID",
fmt)
show_port_res = show_port_req.get_response(self.api)
self.assertEqual(show_port_res.status_int, 430)
self.assertEqual(show_port_res.status_int,
self._port_not_found_code)
LOG.debug("_test_show_port_portnotfound - fmt:%s - END", fmt)
def _test_create_port_noreqbody(self, fmt):
@ -463,7 +469,7 @@ class AbstractAPITest(unittest.TestCase):
fmt)
port_state = "ACTIVE"
self._create_port("A_BAD_ID", port_state, fmt,
expected_res_status=420)
expected_res_status=self._network_not_found_code)
LOG.debug("_test_create_port_networknotfound - fmt:%s - END",
fmt)
@ -520,7 +526,8 @@ class AbstractAPITest(unittest.TestCase):
network_id, port_id,
fmt)
delete_port_res = delete_port_req.get_response(self.api)
self.assertEqual(delete_port_res.status_int, 432)
self.assertEqual(delete_port_res.status_int,
self._port_in_use_code)
LOG.debug("_test_delete_port_in_use - fmt:%s - END", fmt)
def _test_delete_port_with_bad_id(self, fmt):
@ -534,7 +541,8 @@ class AbstractAPITest(unittest.TestCase):
network_id, "A_BAD_ID",
fmt)
delete_port_res = delete_port_req.get_response(self.api)
self.assertEqual(delete_port_res.status_int, 430)
self.assertEqual(delete_port_res.status_int,
self._port_not_found_code)
LOG.debug("_test_delete_port_with_bad_id - fmt:%s - END", fmt)
def _test_delete_port_networknotfound(self, fmt):
@ -547,7 +555,8 @@ class AbstractAPITest(unittest.TestCase):
"A_BAD_ID", port_id,
fmt)
delete_port_res = delete_port_req.get_response(self.api)
self.assertEqual(delete_port_res.status_int, 420)
self.assertEqual(delete_port_res.status_int,
self._network_not_found_code)
LOG.debug("_test_delete_port_networknotfound - fmt:%s - END",
fmt)
@ -603,7 +612,8 @@ class AbstractAPITest(unittest.TestCase):
new_port_state,
fmt)
update_port_res = update_port_req.get_response(self.api)
self.assertEqual(update_port_res.status_int, 420)
self.assertEqual(update_port_res.status_int,
self._network_not_found_code)
LOG.debug("_test_set_port_state_networknotfound - fmt:%s - END",
fmt)
@ -620,7 +630,8 @@ class AbstractAPITest(unittest.TestCase):
new_port_state,
fmt)
update_port_res = update_port_req.get_response(self.api)
self.assertEqual(update_port_res.status_int, 430)
self.assertEqual(update_port_res.status_int,
self._port_not_found_code)
LOG.debug("_test_set_port_state_portnotfound - fmt:%s - END",
fmt)
@ -636,7 +647,8 @@ class AbstractAPITest(unittest.TestCase):
new_port_state,
fmt)
update_port_res = update_port_req.get_response(self.api)
self.assertEqual(update_port_res.status_int, 431)
self.assertEqual(update_port_res.status_int,
self._port_state_invalid_code)
LOG.debug("_test_set_port_state_stateinvalid - fmt:%s - END",
fmt)
@ -691,7 +703,8 @@ class AbstractAPITest(unittest.TestCase):
port_id,
fmt)
get_attachment_res = get_attachment_req.get_response(self.api)
self.assertEqual(get_attachment_res.status_int, 420)
self.assertEqual(get_attachment_res.status_int,
self._network_not_found_code)
LOG.debug("_test_show_attachment_networknotfound - fmt:%s - END",
fmt)
@ -706,7 +719,8 @@ class AbstractAPITest(unittest.TestCase):
"A_BAD_ID",
fmt)
get_attachment_res = get_attachment_req.get_response(self.api)
self.assertEqual(get_attachment_res.status_int, 430)
self.assertEqual(get_attachment_res.status_int,
self._port_not_found_code)
LOG.debug("_test_show_attachment_portnotfound - fmt:%s - END",
fmt)
@ -738,7 +752,8 @@ class AbstractAPITest(unittest.TestCase):
interface_id,
fmt)
put_attachment_res = put_attachment_req.get_response(self.api)
self.assertEqual(put_attachment_res.status_int, 420)
self.assertEqual(put_attachment_res.status_int,
self._network_not_found_code)
LOG.debug("_test_put_attachment_networknotfound - fmt:%s - END",
fmt)
@ -755,7 +770,8 @@ class AbstractAPITest(unittest.TestCase):
interface_id,
fmt)
put_attachment_res = put_attachment_req.get_response(self.api)
self.assertEqual(put_attachment_res.status_int, 430)
self.assertEqual(put_attachment_res.status_int,
self._port_not_found_code)
LOG.debug("_test_put_attachment_portnotfound - fmt:%s - END",
fmt)
@ -791,7 +807,8 @@ class AbstractAPITest(unittest.TestCase):
port_id,
fmt)
del_attachment_res = del_attachment_req.get_response(self.api)
self.assertEqual(del_attachment_res.status_int, 420)
self.assertEqual(del_attachment_res.status_int,
self._network_not_found_code)
LOG.debug("_test_delete_attachment_networknotfound -" \
" fmt:%s - END", fmt)
@ -806,7 +823,8 @@ class AbstractAPITest(unittest.TestCase):
"A_BAD_ID",
fmt)
del_attachment_res = del_attachment_req.get_response(self.api)
self.assertEqual(del_attachment_res.status_int, 430)
self.assertEqual(del_attachment_res.status_int,
self._port_not_found_code)
LOG.debug("_test_delete_attachment_portnotfound - " \
"fmt:%s - END", fmt)

View File

@ -16,6 +16,8 @@
# under the License.
# @author: Salvatore Orlando, Citrix Systems
from webob import exc
import quantum.api.attachments as atts
import quantum.api.networks as nets
import quantum.api.ports as ports
@ -53,6 +55,12 @@ class APITestV10(test_api.AbstractAPITest):
{test_api.NETS: nets.ControllerV10._serialization_metadata,
test_api.PORTS: ports.ControllerV10._serialization_metadata,
test_api.ATTS: atts.ControllerV10._serialization_metadata})
self._network_not_found_code = 420
self._network_in_use_code = 421
self._port_not_found_code = 430
self._port_state_invalid_code = 431
self._port_in_use_code = 432
self._already_attached_code = 440
class APITestV11(test_api.AbstractAPITest):
@ -93,3 +101,9 @@ class APITestV11(test_api.AbstractAPITest):
{test_api.NETS: nets.ControllerV11._serialization_metadata,
test_api.PORTS: ports.ControllerV11._serialization_metadata,
test_api.ATTS: atts.ControllerV11._serialization_metadata})
self._network_not_found_code = exc.HTTPNotFound.code
self._network_in_use_code = exc.HTTPConflict.code
self._port_not_found_code = exc.HTTPNotFound.code
self._port_state_invalid_code = exc.HTTPBadRequest.code
self._port_in_use_code = exc.HTTPConflict.code
self._already_attached_code = exc.HTTPConflict.code

View File

@ -67,7 +67,7 @@ class ResourceExtensionTest(unittest.TestCase):
return {'data': {'id': id}}
def notimplemented_function(self, request, id):
return faults.QuantumHTTPError(
return faults.Quantum10HTTPError(
exceptions.NotImplementedError("notimplemented_function"))
def custom_member_action(self, request, id):

View File

@ -701,18 +701,23 @@ class Resource(Application):
"""
def __init__(self, controller, deserializer=None, serializer=None):
def __init__(self, controller, fault_body_function,
deserializer=None, serializer=None):
"""
:param controller: object that implement methods created by routes lib
:param deserializer: object that can serialize the output of a
controller into a webob response
:param serializer: object that can deserialize a webob request
into necessary pieces
:param fault_body_function: a function that will build the response
body for HTTP errors raised by operations
on this resource object
"""
self.controller = controller
self.deserializer = deserializer or RequestDeserializer()
self.serializer = serializer or ResponseSerializer()
self._fault_body_function = fault_body_function
# use serializer's xmlns for populating Fault generator xmlns
xml_serializer = self.serializer.body_serializers['application/xml']
if hasattr(xml_serializer, 'xmlns'):
@ -742,7 +747,9 @@ class Resource(Application):
action_result = self.dispatch(request, action, args)
except webob.exc.HTTPException as ex:
LOG.info(_("HTTP exception thrown: %s"), unicode(ex))
action_result = Fault(ex, self._xmlns)
action_result = Fault(ex,
self._xmlns,
self._fault_body_function)
if isinstance(action_result, dict) or action_result is None:
response = self.serializer.serialize(action_result,
@ -776,29 +783,32 @@ class Resource(Application):
self._xmlns)
def _default_body_function(wrapped_exc):
code = wrapped_exc.status_int
fault_data = {
'Error': {
'code': code,
'message': wrapped_exc.explanation}}
# 'code' is an attribute on the fault tag itself
metadata = {'attributes': {'Error': 'code'}}
return fault_data, metadata
class Fault(webob.exc.HTTPException):
""" Generates an HTTP response from a webob HTTP exception"""
def __init__(self, exception, xmlns=None):
def __init__(self, exception, xmlns=None, body_function=None):
"""Creates a Fault for the given webob.exc.exception."""
self.wrapped_exc = exception
self.status_int = self.wrapped_exc.status_int
self._xmlns = xmlns
self._body_function = body_function or _default_body_function
@webob.dec.wsgify(RequestClass=Request)
def __call__(self, req):
"""Generate a WSGI response based on the exception passed to ctor."""
# Replace the body with fault details.
code = self.wrapped_exc.status_int
fault_name = hasattr(self.wrapped_exc, 'title') and \
self.wrapped_exc.title or "quantumServiceFault"
fault_data = {
fault_name: {
'code': code,
'message': self.wrapped_exc.explanation,
'detail': str(self.wrapped_exc.detail)}}
# 'code' is an attribute on the fault tag itself
metadata = {'application/xml': {'attributes': {fault_name: 'code'}}}
fault_data, metadata = self._body_function(self.wrapped_exc)
xml_serializer = XMLDictSerializer(metadata, self._xmlns)
content_type = req.best_match_content_type()
serializer = {