blueprint api-framework-essex
Addressing Dan's comments. This changeset provides: - improved framework for API versioning, with separated controllers for each API version - Taken from nova:'Resource' class in WSGI framework. This class is a container for serializers, deserializers and controller - Better deserialization thanks to resource class. _parse_request_params has been removed - Improved management of HTTP client errors NOTE: this changeset does not update the framework used by API extensions. Change-Id: I88a669ce418225c415e0da22e951762d0708e0a5
This commit is contained in:
parent
39eaed52fa
commit
5698d9b3f8
@ -21,14 +21,22 @@ api_extensions_path = server/lib/quantum/extensions
|
||||
[composite:quantum]
|
||||
use = egg:Paste#urlmap
|
||||
/: quantumversions
|
||||
/v1.0: quantumapi
|
||||
/v1.0: quantumapi_v1_0
|
||||
/v1.1: quantumapi_v1_1
|
||||
|
||||
[pipeline:quantumapi]
|
||||
[pipeline:quantumapi_v1_0]
|
||||
# By default, authentication is disabled.
|
||||
# To enable Keystone integration uncomment the
|
||||
# following line and comment the next one
|
||||
pipeline = extensions quantumapiapp
|
||||
#pipeline = authN extensions quantumapiapp
|
||||
pipeline = extensions quantumapiapp_v1_0
|
||||
#pipeline = authN extensions quantumapiapp_v1_0
|
||||
|
||||
[pipeline:quantumapi_v1_1]
|
||||
# By default, authentication is disabled.
|
||||
# To enable Keystone integration uncomment the
|
||||
# following line and comment the next one
|
||||
pipeline = extensions quantumapiapp_v1_1
|
||||
#pipeline = authN extensions quantumapiapp_v1_1
|
||||
|
||||
[filter:authN]
|
||||
paste.filter_factory = keystone.middleware.quantum_auth_token:filter_factory
|
||||
@ -46,5 +54,8 @@ paste.filter_factory = quantum.common.extensions:plugin_aware_extension_middlewa
|
||||
[app:quantumversions]
|
||||
paste.app_factory = quantum.api.versions:Versions.factory
|
||||
|
||||
[app:quantumapiapp]
|
||||
paste.app_factory = quantum.api:APIRouterV1.factory
|
||||
[app:quantumapiapp_v1_0]
|
||||
paste.app_factory = quantum.api:APIRouterV10.factory
|
||||
|
||||
[app:quantumapiapp_v1_1]
|
||||
paste.app_factory = quantum.api:APIRouterV11.factory
|
||||
|
@ -25,7 +25,6 @@ import webob.dec
|
||||
import webob.exc
|
||||
|
||||
from quantum import manager
|
||||
from quantum.api import faults
|
||||
from quantum.api import attachments
|
||||
from quantum.api import networks
|
||||
from quantum.api import ports
|
||||
@ -37,34 +36,43 @@ LOG = logging.getLogger('quantum.api')
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
|
||||
class APIRouterV1(wsgi.Router):
|
||||
class APIRouter(wsgi.Router):
|
||||
"""
|
||||
Routes requests on the Quantum API to the appropriate controller
|
||||
Base class for Quantum API routes.
|
||||
"""
|
||||
|
||||
def __init__(self, options=None):
|
||||
mapper = routes.Mapper()
|
||||
mapper = self._mapper()
|
||||
self._setup_routes(mapper, options)
|
||||
super(APIRouterV1, self).__init__(mapper)
|
||||
super(APIRouter, self).__init__(mapper)
|
||||
|
||||
def _mapper(self):
|
||||
return routes.Mapper()
|
||||
|
||||
def _setup_routes(self, mapper, options):
|
||||
self._setup_base_routes(mapper, options, self._version)
|
||||
|
||||
def _setup_base_routes(self, mapper, options, version):
|
||||
"""Routes common to all versions."""
|
||||
# Loads the quantum plugin
|
||||
# Note(salvatore-orlando): Should the plugin be versioned
|
||||
# I don't think so
|
||||
plugin = manager.QuantumManager.get_plugin(options)
|
||||
|
||||
uri_prefix = '/tenants/{tenant_id}/'
|
||||
mapper.resource('network', 'networks',
|
||||
controller=networks.Controller(plugin),
|
||||
controller=networks.create_resource(plugin, version),
|
||||
collection={'detail': 'GET'},
|
||||
member={'detail': 'GET'},
|
||||
path_prefix=uri_prefix)
|
||||
mapper.resource('port', 'ports',
|
||||
controller=ports.Controller(plugin),
|
||||
controller=ports.create_resource(plugin, version),
|
||||
collection={'detail': 'GET'},
|
||||
member={'detail': 'GET'},
|
||||
parent_resource=dict(member_name='network',
|
||||
collection_name=uri_prefix +\
|
||||
'networks'))
|
||||
attachments_ctrl = attachments.Controller(plugin)
|
||||
attachments_ctrl = attachments.create_resource(plugin, version)
|
||||
mapper.connect("get_resource",
|
||||
uri_prefix + 'networks/{network_id}/' \
|
||||
'ports/{id}/attachment{.format}',
|
||||
@ -83,3 +91,17 @@ class APIRouterV1(wsgi.Router):
|
||||
controller=attachments_ctrl,
|
||||
action="detach_resource",
|
||||
conditions=dict(method=['DELETE']))
|
||||
|
||||
|
||||
class APIRouterV10(APIRouter):
|
||||
"""
|
||||
API routes mappings for Quantum API v1.0
|
||||
"""
|
||||
_version = '1.0'
|
||||
|
||||
|
||||
class APIRouterV11(APIRouter):
|
||||
"""
|
||||
API routes mappings for Quantum API v1.1
|
||||
"""
|
||||
_version = '1.1'
|
||||
|
@ -16,39 +16,122 @@
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
import webob
|
||||
|
||||
from webob import exc
|
||||
|
||||
from quantum import wsgi
|
||||
from quantum.api import faults
|
||||
|
||||
XML_NS_V01 = 'http://netstack.org/quantum/api/v0.1'
|
||||
XML_NS_V10 = 'http://netstack.org/quantum/api/v1.0'
|
||||
XML_NS_V10 = 'http://openstack.org/quantum/api/v1.0'
|
||||
XML_NS_V11 = 'http://openstack.org/quantum/api/v1.1'
|
||||
LOG = logging.getLogger('quantum.api.api_common')
|
||||
|
||||
|
||||
class QuantumController(wsgi.Controller):
|
||||
def create_resource(version, controller_dict):
|
||||
"""
|
||||
Generic function for creating a wsgi resource
|
||||
The function takes as input:
|
||||
- desired version
|
||||
- controller and metadata dictionary
|
||||
e.g.: {'1.0': [ctrl_v10, meta_v10, xml_ns],
|
||||
'1.1': [ctrl_v11, meta_v11, xml_ns]}
|
||||
|
||||
"""
|
||||
# the first element of the iterable is expected to be the controller
|
||||
controller = controller_dict[version][0]
|
||||
# the second element should be the metadata
|
||||
metadata = controller_dict[version][1]
|
||||
# and the third element the xml namespace
|
||||
xmlns = controller_dict[version][2]
|
||||
|
||||
headers_serializer = HeaderSerializer()
|
||||
xml_serializer = wsgi.XMLDictSerializer(metadata, xmlns)
|
||||
json_serializer = wsgi.JSONDictSerializer()
|
||||
xml_deserializer = wsgi.XMLDeserializer(metadata)
|
||||
json_deserializer = wsgi.JSONDeserializer()
|
||||
|
||||
body_serializers = {
|
||||
'application/xml': xml_serializer,
|
||||
'application/json': json_serializer,
|
||||
}
|
||||
|
||||
body_deserializers = {
|
||||
'application/xml': xml_deserializer,
|
||||
'application/json': json_deserializer,
|
||||
}
|
||||
|
||||
serializer = wsgi.ResponseSerializer(body_serializers, headers_serializer)
|
||||
deserializer = wsgi.RequestDeserializer(body_deserializers)
|
||||
|
||||
return wsgi.Resource(controller, deserializer, serializer)
|
||||
|
||||
|
||||
def APIFaultWrapper(errors=None):
|
||||
|
||||
def wrapper(func, **kwargs):
|
||||
|
||||
def the_func(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
if errors != None and type(e) in errors:
|
||||
raise faults.QuantumHTTPError(e)
|
||||
# otherwise just re-raise
|
||||
raise
|
||||
the_func.__name__ = func.__name__
|
||||
return the_func
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class HeaderSerializer(wsgi.ResponseHeaderSerializer):
|
||||
"""
|
||||
Defines default respone status codes for Quantum API operations
|
||||
create - 202 ACCEPTED
|
||||
update - 204 NOCONTENT
|
||||
delete - 204 NOCONTENT
|
||||
others - 200 OK (defined in base class)
|
||||
|
||||
"""
|
||||
|
||||
def create(self, response, data):
|
||||
response.status_int = 202
|
||||
|
||||
def delete(self, response, data):
|
||||
response.status_int = 204
|
||||
|
||||
def update(self, response, data):
|
||||
response.status_int = 204
|
||||
|
||||
def attach_resource(self, response, data):
|
||||
response.status_int = 204
|
||||
|
||||
def detach_resource(self, response, data):
|
||||
response.status_int = 204
|
||||
|
||||
|
||||
class QuantumController(object):
|
||||
""" Base controller class for Quantum API """
|
||||
|
||||
def __init__(self, plugin):
|
||||
self._plugin = plugin
|
||||
super(QuantumController, self).__init__()
|
||||
|
||||
def _parse_request_params(self, req, params):
|
||||
results = {}
|
||||
data = {}
|
||||
# Parameters are expected to be in request body only
|
||||
if req.body:
|
||||
des_body = self._deserialize(req.body,
|
||||
req.best_match_content_type())
|
||||
data = des_body and des_body.get(self._resource_name, None)
|
||||
if not data:
|
||||
msg = ("Failed to parse request. Resource: " +
|
||||
self._resource_name + " not found in request body")
|
||||
for line in msg.split('\n'):
|
||||
LOG.error(line)
|
||||
raise exc.HTTPBadRequest(msg)
|
||||
def _prepare_request_body(self, body, params):
|
||||
""" verifies required parameters are in request body.
|
||||
sets default value for missing optional parameters.
|
||||
|
||||
body argument must be the deserialized body
|
||||
"""
|
||||
try:
|
||||
if body is None:
|
||||
# Initialize empty resource for setting default value
|
||||
body = {self._resource_name: {}}
|
||||
data = body[self._resource_name]
|
||||
except KeyError:
|
||||
# raise if _resource_name is not in req body.
|
||||
raise exc.HTTPBadRequest("Unable to find '%s' in request body"\
|
||||
% self._resource_name)
|
||||
for param in params:
|
||||
param_name = param['param-name']
|
||||
param_value = data.get(param_name, None)
|
||||
@ -59,31 +142,5 @@ class QuantumController(wsgi.Controller):
|
||||
for line in msg.split('\n'):
|
||||
LOG.error(line)
|
||||
raise exc.HTTPBadRequest(msg)
|
||||
results[param_name] = param_value or param.get('default-value')
|
||||
|
||||
# There may be other parameters (data extensions), so we
|
||||
# should include those in the results dict as well.
|
||||
for key in data.keys():
|
||||
if key not in params:
|
||||
results[key] = data[key]
|
||||
|
||||
return results
|
||||
|
||||
def _build_response(self, req, res_data, status_code=200):
|
||||
""" A function which builds an HTTP response
|
||||
given a status code and a dictionary containing
|
||||
the response body to be serialized
|
||||
|
||||
"""
|
||||
content_type = req.best_match_content_type()
|
||||
default_xmlns = self.get_default_xmlns(req)
|
||||
body = self._serialize(res_data, content_type, default_xmlns)
|
||||
|
||||
response = webob.Response()
|
||||
response.status = status_code
|
||||
response.headers['Content-Type'] = content_type
|
||||
response.body = body
|
||||
msg_dict = dict(url=req.url, status=response.status_int)
|
||||
msg = _("%(url)s returned with HTTP %(status)d") % msg_dict
|
||||
LOG.debug(msg)
|
||||
return response
|
||||
data[param_name] = param_value or param.get('default-value')
|
||||
return body
|
||||
|
@ -15,16 +15,25 @@
|
||||
|
||||
import logging
|
||||
|
||||
from webob import exc
|
||||
|
||||
from quantum.api import api_common as common
|
||||
from quantum.api import faults
|
||||
from quantum.api.views import attachments as attachments_view
|
||||
from quantum.common import exceptions as exception
|
||||
|
||||
|
||||
LOG = logging.getLogger('quantum.api.ports')
|
||||
|
||||
|
||||
def create_resource(plugin, version):
|
||||
controller_dict = {
|
||||
'1.0': [ControllerV10(plugin),
|
||||
ControllerV10._serialization_metadata,
|
||||
common.XML_NS_V10],
|
||||
'1.1': [ControllerV11(plugin),
|
||||
ControllerV11._serialization_metadata,
|
||||
common.XML_NS_V11]}
|
||||
return common.create_resource(version, controller_dict)
|
||||
|
||||
|
||||
class Controller(common.QuantumController):
|
||||
""" Port API controller for Quantum API """
|
||||
|
||||
@ -43,45 +52,43 @@ class Controller(common.QuantumController):
|
||||
self._resource_name = 'attachment'
|
||||
super(Controller, self).__init__(plugin)
|
||||
|
||||
@common.APIFaultWrapper([exception.NetworkNotFound,
|
||||
exception.PortNotFound])
|
||||
def get_resource(self, request, tenant_id, network_id, id):
|
||||
try:
|
||||
att_data = self._plugin.get_port_details(
|
||||
tenant_id, network_id, id)
|
||||
builder = attachments_view.get_view_builder(request)
|
||||
result = builder.build(att_data)['attachment']
|
||||
return dict(attachment=result)
|
||||
except exception.NetworkNotFound as e:
|
||||
return faults.Fault(faults.NetworkNotFound(e))
|
||||
except exception.PortNotFound as e:
|
||||
return faults.Fault(faults.PortNotFound(e))
|
||||
att_data = self._plugin.get_port_details(
|
||||
tenant_id, network_id, id)
|
||||
builder = attachments_view.get_view_builder(request)
|
||||
result = builder.build(att_data)['attachment']
|
||||
return dict(attachment=result)
|
||||
|
||||
def attach_resource(self, request, tenant_id, network_id, id):
|
||||
try:
|
||||
request_params = \
|
||||
self._parse_request_params(request,
|
||||
self._attachment_ops_param_list)
|
||||
except exc.HTTPError as e:
|
||||
return faults.Fault(e)
|
||||
try:
|
||||
LOG.debug("PLUGGING INTERFACE:%s", request_params['id'])
|
||||
self._plugin.plug_interface(tenant_id, network_id, id,
|
||||
request_params['id'])
|
||||
return exc.HTTPNoContent()
|
||||
except exception.NetworkNotFound as e:
|
||||
return faults.Fault(faults.NetworkNotFound(e))
|
||||
except exception.PortNotFound as e:
|
||||
return faults.Fault(faults.PortNotFound(e))
|
||||
except exception.PortInUse as e:
|
||||
return faults.Fault(faults.PortInUse(e))
|
||||
except exception.AlreadyAttached as e:
|
||||
return faults.Fault(faults.AlreadyAttached(e))
|
||||
@common.APIFaultWrapper([exception.NetworkNotFound,
|
||||
exception.PortNotFound,
|
||||
exception.PortInUse,
|
||||
exception.AlreadyAttached])
|
||||
def attach_resource(self, request, tenant_id, network_id, id, body):
|
||||
body = self._prepare_request_body(body,
|
||||
self._attachment_ops_param_list)
|
||||
self._plugin.plug_interface(tenant_id, network_id, id,
|
||||
body['attachment']['id'])
|
||||
|
||||
@common.APIFaultWrapper([exception.NetworkNotFound,
|
||||
exception.PortNotFound])
|
||||
def detach_resource(self, request, tenant_id, network_id, id):
|
||||
try:
|
||||
self._plugin.unplug_interface(tenant_id,
|
||||
network_id, id)
|
||||
return exc.HTTPNoContent()
|
||||
except exception.NetworkNotFound as e:
|
||||
return faults.Fault(faults.NetworkNotFound(e))
|
||||
except exception.PortNotFound as e:
|
||||
return faults.Fault(faults.PortNotFound(e))
|
||||
self._plugin.unplug_interface(tenant_id,
|
||||
network_id, id)
|
||||
|
||||
|
||||
class ControllerV10(Controller):
|
||||
"""Attachment resources controller for Quantum v1.0 API"""
|
||||
|
||||
def __init__(self, plugin):
|
||||
self.version = "1.0"
|
||||
super(ControllerV10, self).__init__(plugin)
|
||||
|
||||
|
||||
class ControllerV11(Controller):
|
||||
"""Attachment resources controller for Quantum v1.1 API"""
|
||||
|
||||
def __init__(self, plugin):
|
||||
self.version = "1.1"
|
||||
super(ControllerV11, self).__init__(plugin)
|
||||
|
@ -16,132 +16,57 @@
|
||||
# under the License.
|
||||
|
||||
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
|
||||
from quantum.api import api_common as common
|
||||
from quantum import wsgi
|
||||
from quantum.common import exceptions
|
||||
|
||||
_NETNOTFOUND_EXPL = 'Unable to find a network with the specified identifier.'
|
||||
_NETINUSE_EXPL = 'Unable to remove the network: attachments still plugged.'
|
||||
_PORTNOTFOUND_EXPL = 'Unable to find a port with the specified identifier.'
|
||||
_STATEINVALID_EXPL = 'Unable to update port state with specified value.'
|
||||
_PORTINUSE_EXPL = 'A resource is currently attached to the logical port'
|
||||
_ALREADYATTACHED_EXPL = 'The resource is already attached to another port'
|
||||
|
||||
|
||||
class Fault(webob.exc.HTTPException):
|
||||
"""Error codes for API faults"""
|
||||
class QuantumHTTPError(webob.exc.HTTPClientError):
|
||||
|
||||
_fault_names = {
|
||||
400: "malformedRequest",
|
||||
401: "unauthorized",
|
||||
420: "networkNotFound",
|
||||
421: "networkInUse",
|
||||
430: "portNotFound",
|
||||
431: "requestedStateInvalid",
|
||||
432: "portInUse",
|
||||
440: "alreadyAttached",
|
||||
470: "serviceUnavailable",
|
||||
471: "pluginFault"}
|
||||
_fault_dict = {
|
||||
exceptions.NetworkNotFound: {
|
||||
'code': 420,
|
||||
'title': 'networkNotFound',
|
||||
'explanation': _NETNOTFOUND_EXPL
|
||||
},
|
||||
exceptions.NetworkInUse: {
|
||||
'code': 421,
|
||||
'title': 'networkInUse',
|
||||
'explanation': _NETINUSE_EXPL
|
||||
},
|
||||
exceptions.PortNotFound: {
|
||||
'code': 430,
|
||||
'title': 'portNotFound',
|
||||
'explanation': _PORTNOTFOUND_EXPL
|
||||
},
|
||||
exceptions.StateInvalid: {
|
||||
'code': 431,
|
||||
'title': 'requestedStateInvalid',
|
||||
'explanation': _STATEINVALID_EXPL
|
||||
},
|
||||
exceptions.PortInUse: {
|
||||
'code': 432,
|
||||
'title': 'portInUse',
|
||||
'explanation': _PORTINUSE_EXPL
|
||||
},
|
||||
exceptions.AlreadyAttached: {
|
||||
'code': 440,
|
||||
'title': 'alreadyAttached',
|
||||
'explanation': _ALREADYATTACHED_EXPL
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, exception):
|
||||
"""Create a Fault for the given webob.exc.exception."""
|
||||
self.wrapped_exc = exception
|
||||
|
||||
@webob.dec.wsgify(RequestClass=wsgi.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 = self._fault_names.get(code, "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'}}}
|
||||
default_xmlns = common.XML_NS_V10
|
||||
serializer = wsgi.Serializer(metadata, default_xmlns)
|
||||
content_type = req.best_match_content_type()
|
||||
self.wrapped_exc.body = serializer.serialize(fault_data, content_type)
|
||||
self.wrapped_exc.content_type = content_type
|
||||
return self.wrapped_exc
|
||||
|
||||
|
||||
class NetworkNotFound(webob.exc.HTTPClientError):
|
||||
"""
|
||||
subclass of :class:`~HTTPClientError`
|
||||
|
||||
This indicates that the server did not find the network specified
|
||||
in the HTTP request
|
||||
|
||||
code: 420, title: Network not Found
|
||||
"""
|
||||
code = 420
|
||||
title = 'Network not Found'
|
||||
explanation = ('Unable to find a network with the specified identifier.')
|
||||
|
||||
|
||||
class NetworkInUse(webob.exc.HTTPClientError):
|
||||
"""
|
||||
subclass of :class:`~HTTPClientError`
|
||||
|
||||
This indicates that the server could not delete the network as there is
|
||||
at least an attachment plugged into its ports
|
||||
|
||||
code: 421, title: Network In Use
|
||||
"""
|
||||
code = 421
|
||||
title = 'Network in Use'
|
||||
explanation = ('Unable to remove the network: attachments still plugged.')
|
||||
|
||||
|
||||
class PortNotFound(webob.exc.HTTPClientError):
|
||||
"""
|
||||
subclass of :class:`~HTTPClientError`
|
||||
|
||||
This indicates that the server did not find the port specified
|
||||
in the HTTP request for a given network
|
||||
|
||||
code: 430, title: Port not Found
|
||||
"""
|
||||
code = 430
|
||||
title = 'Port not Found'
|
||||
explanation = ('Unable to find a port with the specified identifier.')
|
||||
|
||||
|
||||
class RequestedStateInvalid(webob.exc.HTTPClientError):
|
||||
"""
|
||||
subclass of :class:`~HTTPClientError`
|
||||
|
||||
This indicates that the server could not update the port state to
|
||||
to the request value
|
||||
|
||||
code: 431, title: Requested State Invalid
|
||||
"""
|
||||
code = 431
|
||||
title = 'Requested State Invalid'
|
||||
explanation = ('Unable to update port state with specified value.')
|
||||
|
||||
|
||||
class PortInUse(webob.exc.HTTPClientError):
|
||||
"""
|
||||
subclass of :class:`~HTTPClientError`
|
||||
|
||||
This indicates that the server could not remove o port or attach
|
||||
a resource to it because there is an attachment plugged into the port
|
||||
|
||||
code: 432, title: PortInUse
|
||||
"""
|
||||
code = 432
|
||||
title = 'Port in Use'
|
||||
explanation = ('A resource is currently attached to the logical port')
|
||||
|
||||
|
||||
class AlreadyAttached(webob.exc.HTTPClientError):
|
||||
"""
|
||||
subclass of :class:`~HTTPClientError`
|
||||
|
||||
This indicates that the server refused an attempt to re-attach a resource
|
||||
already attached to the network
|
||||
|
||||
code: 440, title: AlreadyAttached
|
||||
"""
|
||||
code = 440
|
||||
title = 'Already Attached'
|
||||
explanation = ('The resource is already attached to another port')
|
||||
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']
|
||||
super(webob.exc.HTTPClientError, self).__init__(inner_exc)
|
||||
|
@ -25,6 +25,17 @@ from quantum.common import exceptions as exception
|
||||
LOG = logging.getLogger('quantum.api.networks')
|
||||
|
||||
|
||||
def create_resource(plugin, version):
|
||||
controller_dict = {
|
||||
'1.0': [ControllerV10(plugin),
|
||||
ControllerV10._serialization_metadata,
|
||||
common.XML_NS_V10],
|
||||
'1.1': [ControllerV11(plugin),
|
||||
ControllerV11._serialization_metadata,
|
||||
common.XML_NS_V11]}
|
||||
return common.create_resource(version, controller_dict)
|
||||
|
||||
|
||||
class Controller(common.QuantumController):
|
||||
""" Network API controller for Quantum API """
|
||||
|
||||
@ -32,21 +43,11 @@ class Controller(common.QuantumController):
|
||||
'param-name': 'name',
|
||||
'required': True}, ]
|
||||
|
||||
_serialization_metadata = {
|
||||
"application/xml": {
|
||||
"attributes": {
|
||||
"network": ["id", "name"],
|
||||
"port": ["id", "state"],
|
||||
"attachment": ["id"]},
|
||||
"plurals": {"networks": "network",
|
||||
"ports": "port"}},
|
||||
}
|
||||
|
||||
def __init__(self, plugin):
|
||||
self._resource_name = 'network'
|
||||
super(Controller, self).__init__(plugin)
|
||||
|
||||
def _item(self, req, tenant_id, network_id,
|
||||
def _item(self, request, tenant_id, network_id,
|
||||
net_details=True, port_details=False):
|
||||
# We expect get_network_details to return information
|
||||
# concerning logical ports as well.
|
||||
@ -57,31 +58,34 @@ class Controller(common.QuantumController):
|
||||
ports_data = [self._plugin.get_port_details(
|
||||
tenant_id, network_id, port['port-id'])
|
||||
for port in port_list]
|
||||
builder = networks_view.get_view_builder(req)
|
||||
builder = networks_view.get_view_builder(request, self.version)
|
||||
result = builder.build(network, net_details,
|
||||
ports_data, port_details)['network']
|
||||
return dict(network=result)
|
||||
|
||||
def _items(self, req, tenant_id, net_details=False):
|
||||
def _items(self, request, tenant_id, net_details=False):
|
||||
""" Returns a list of networks. """
|
||||
networks = self._plugin.get_all_networks(tenant_id)
|
||||
builder = networks_view.get_view_builder(req)
|
||||
builder = networks_view.get_view_builder(request, self.version)
|
||||
result = [builder.build(network, net_details)['network']
|
||||
for network in networks]
|
||||
return dict(networks=result)
|
||||
|
||||
@common.APIFaultWrapper()
|
||||
def index(self, request, tenant_id):
|
||||
""" Returns a list of network ids """
|
||||
return self._items(request, tenant_id)
|
||||
|
||||
@common.APIFaultWrapper()
|
||||
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:
|
||||
return faults.Fault(faults.NetworkNotFound(e))
|
||||
raise faults.QuantumHTTPError(e)
|
||||
|
||||
@common.APIFaultWrapper()
|
||||
def detail(self, request, **kwargs):
|
||||
tenant_id = kwargs.get('tenant_id')
|
||||
network_id = kwargs.get('id')
|
||||
@ -93,53 +97,66 @@ class Controller(common.QuantumController):
|
||||
# show details for all networks
|
||||
return self._items(request, tenant_id, net_details=True)
|
||||
|
||||
def create(self, request, tenant_id):
|
||||
@common.APIFaultWrapper()
|
||||
def create(self, request, tenant_id, body):
|
||||
""" Creates a new network for a given tenant """
|
||||
try:
|
||||
request_params = \
|
||||
self._parse_request_params(request,
|
||||
self._network_ops_param_list)
|
||||
except exc.HTTPError as e:
|
||||
return faults.Fault(e)
|
||||
# NOTE(bgh): We're currently passing both request_params['name'] and
|
||||
# the entire request_params dict because their may be pieces of
|
||||
# information (data extensions) inside the request params that the
|
||||
# actual plugin will want to parse. We could just pass only
|
||||
# request_params but that would mean all the plugins would need to
|
||||
# change.
|
||||
body = self._prepare_request_body(body, self._network_ops_param_list)
|
||||
network = self._plugin.\
|
||||
create_network(tenant_id,
|
||||
request_params['name'],
|
||||
**request_params)
|
||||
builder = networks_view.get_view_builder(request)
|
||||
body['network']['name'],
|
||||
**body)
|
||||
builder = networks_view.get_view_builder(request, self.version)
|
||||
result = builder.build(network)['network']
|
||||
# Wsgi middleware allows us to build the response
|
||||
# before returning the call.
|
||||
# This will allow us to return a 200 status code. NOTE: in v1.1 we
|
||||
# will be returning a 202 status code.
|
||||
return self._build_response(request, dict(network=result), 200)
|
||||
return dict(network=result)
|
||||
|
||||
def update(self, request, tenant_id, id):
|
||||
@common.APIFaultWrapper([exception.NetworkNotFound])
|
||||
def update(self, request, tenant_id, id, body):
|
||||
""" Updates the name for the network with the given id """
|
||||
try:
|
||||
request_params = \
|
||||
self._parse_request_params(request,
|
||||
self._network_ops_param_list)
|
||||
except exc.HTTPError as e:
|
||||
return faults.Fault(e)
|
||||
try:
|
||||
self._plugin.update_network(tenant_id, id,
|
||||
**request_params)
|
||||
return exc.HTTPNoContent()
|
||||
except exception.NetworkNotFound as e:
|
||||
return faults.Fault(faults.NetworkNotFound(e))
|
||||
body = self._prepare_request_body(body, self._network_ops_param_list)
|
||||
self._plugin.update_network(tenant_id, id, **body['network'])
|
||||
|
||||
@common.APIFaultWrapper([exception.NetworkNotFound,
|
||||
exception.NetworkInUse])
|
||||
def delete(self, request, tenant_id, id):
|
||||
""" Destroys the network with the given id """
|
||||
try:
|
||||
self._plugin.delete_network(tenant_id, id)
|
||||
return exc.HTTPNoContent()
|
||||
except exception.NetworkNotFound as e:
|
||||
return faults.Fault(faults.NetworkNotFound(e))
|
||||
except exception.NetworkInUse as e:
|
||||
return faults.Fault(faults.NetworkInUse(e))
|
||||
self._plugin.delete_network(tenant_id, id)
|
||||
|
||||
|
||||
class ControllerV10(Controller):
|
||||
"""Network resources controller for Quantum v1.0 API"""
|
||||
|
||||
_serialization_metadata = {
|
||||
"attributes": {
|
||||
"network": ["id", "name"],
|
||||
"port": ["id", "state"],
|
||||
"attachment": ["id"]},
|
||||
"plurals": {"networks": "network",
|
||||
"ports": "port"}
|
||||
}
|
||||
|
||||
def __init__(self, plugin):
|
||||
self.version = "1.0"
|
||||
super(ControllerV10, self).__init__(plugin)
|
||||
|
||||
|
||||
class ControllerV11(Controller):
|
||||
"""Network resources controller for Quantum v1.1 API"""
|
||||
|
||||
_serialization_metadata = {
|
||||
"attributes": {
|
||||
"network": ["id", "name"],
|
||||
"port": ["id", "state"],
|
||||
"attachment": ["id"]},
|
||||
"plurals": {"networks": "network",
|
||||
"ports": "port"}
|
||||
}
|
||||
|
||||
def __init__(self, plugin):
|
||||
self.version = "1.1"
|
||||
super(ControllerV11, self).__init__(plugin)
|
||||
|
@ -15,16 +15,25 @@
|
||||
|
||||
import logging
|
||||
|
||||
from webob import exc
|
||||
|
||||
from quantum.api import api_common as common
|
||||
from quantum.api import faults
|
||||
from quantum.api.views import ports as ports_view
|
||||
from quantum.common import exceptions as exception
|
||||
|
||||
|
||||
LOG = logging.getLogger('quantum.api.ports')
|
||||
|
||||
|
||||
def create_resource(plugin, version):
|
||||
controller_dict = {
|
||||
'1.0': [ControllerV10(plugin),
|
||||
ControllerV10._serialization_metadata,
|
||||
common.XML_NS_V10],
|
||||
'1.1': [ControllerV11(plugin),
|
||||
ControllerV11._serialization_metadata,
|
||||
common.XML_NS_V11]}
|
||||
return common.create_resource(version, controller_dict)
|
||||
|
||||
|
||||
class Controller(common.QuantumController):
|
||||
""" Port API controller for Quantum API """
|
||||
|
||||
@ -33,14 +42,6 @@ class Controller(common.QuantumController):
|
||||
'default-value': 'DOWN',
|
||||
'required': False}, ]
|
||||
|
||||
_serialization_metadata = {
|
||||
"application/xml": {
|
||||
"attributes": {
|
||||
"port": ["id", "state"],
|
||||
"attachment": ["id"]},
|
||||
"plurals": {"ports": "port"}},
|
||||
}
|
||||
|
||||
def __init__(self, plugin):
|
||||
self._resource_name = 'port'
|
||||
super(Controller, self).__init__(plugin)
|
||||
@ -48,23 +49,20 @@ class Controller(common.QuantumController):
|
||||
def _items(self, request, tenant_id, network_id,
|
||||
port_details=False):
|
||||
""" Returns a list of ports. """
|
||||
try:
|
||||
port_list = self._plugin.get_all_ports(tenant_id, network_id)
|
||||
builder = ports_view.get_view_builder(request)
|
||||
port_list = self._plugin.get_all_ports(tenant_id, network_id)
|
||||
builder = ports_view.get_view_builder(request)
|
||||
|
||||
# Load extra data for ports if required.
|
||||
if port_details:
|
||||
port_list_detail = \
|
||||
[self._plugin.get_port_details(
|
||||
tenant_id, network_id, port['port-id'])
|
||||
for port in port_list]
|
||||
port_list = port_list_detail
|
||||
# Load extra data for ports if required.
|
||||
if port_details:
|
||||
port_list_detail = \
|
||||
[self._plugin.get_port_details(
|
||||
tenant_id, network_id, port['port-id'])
|
||||
for port in port_list]
|
||||
port_list = port_list_detail
|
||||
|
||||
result = [builder.build(port, port_details)['port']
|
||||
for port in port_list]
|
||||
return dict(ports=result)
|
||||
except exception.NetworkNotFound as e:
|
||||
return faults.Fault(faults.NetworkNotFound(e))
|
||||
result = [builder.build(port, port_details)['port']
|
||||
for port in port_list]
|
||||
return dict(ports=result)
|
||||
|
||||
def _item(self, request, tenant_id, network_id, port_id,
|
||||
att_details=False):
|
||||
@ -76,19 +74,19 @@ class Controller(common.QuantumController):
|
||||
att_details=att_details)['port']
|
||||
return dict(port=result)
|
||||
|
||||
@common.APIFaultWrapper([exception.NetworkNotFound])
|
||||
def index(self, request, tenant_id, network_id):
|
||||
""" Returns a list of port ids for a given network """
|
||||
return self._items(request, tenant_id, network_id, port_details=False)
|
||||
|
||||
@common.APIFaultWrapper([exception.NetworkNotFound,
|
||||
exception.PortNotFound])
|
||||
def show(self, request, tenant_id, network_id, id):
|
||||
""" Returns port details for given port and network """
|
||||
try:
|
||||
return self._item(request, tenant_id, network_id, id)
|
||||
except exception.NetworkNotFound as e:
|
||||
return faults.Fault(faults.NetworkNotFound(e))
|
||||
except exception.PortNotFound as e:
|
||||
return faults.Fault(faults.PortNotFound(e))
|
||||
return self._item(request, tenant_id, network_id, id)
|
||||
|
||||
@common.APIFaultWrapper([exception.NetworkNotFound,
|
||||
exception.PortNotFound])
|
||||
def detail(self, request, **kwargs):
|
||||
tenant_id = kwargs.get('tenant_id')
|
||||
network_id = kwargs.get('network_id')
|
||||
@ -102,57 +100,62 @@ class Controller(common.QuantumController):
|
||||
return self._items(request, tenant_id,
|
||||
network_id, port_details=True)
|
||||
|
||||
def create(self, request, tenant_id, network_id):
|
||||
""" Creates a new port for a given network """
|
||||
try:
|
||||
request_params = \
|
||||
self._parse_request_params(request, self._port_ops_param_list)
|
||||
except exc.HTTPError as e:
|
||||
return faults.Fault(e)
|
||||
try:
|
||||
port = self._plugin.create_port(tenant_id,
|
||||
network_id,
|
||||
request_params['state'],
|
||||
**request_params)
|
||||
builder = ports_view.get_view_builder(request)
|
||||
result = builder.build(port)['port']
|
||||
# Wsgi middleware allows us to build the response
|
||||
# before returning the call.
|
||||
# This will allow us to return a 200 status code. NOTE: in v1.1
|
||||
# we will be returning a 202 status code.
|
||||
return self._build_response(request, dict(port=result), 200)
|
||||
except exception.NetworkNotFound as e:
|
||||
return faults.Fault(faults.NetworkNotFound(e))
|
||||
except exception.StateInvalid as e:
|
||||
return faults.Fault(faults.RequestedStateInvalid(e))
|
||||
@common.APIFaultWrapper([exception.NetworkNotFound,
|
||||
exception.StateInvalid])
|
||||
def create(self, request, tenant_id, network_id, body=None):
|
||||
""" Creates a new port for a given network
|
||||
The request body is optional for a port object.
|
||||
|
||||
def update(self, request, tenant_id, network_id, id):
|
||||
"""
|
||||
body = self._prepare_request_body(body, self._port_ops_param_list)
|
||||
port = self._plugin.create_port(tenant_id,
|
||||
network_id, body['port']['state'],
|
||||
**body)
|
||||
builder = ports_view.get_view_builder(request)
|
||||
result = builder.build(port)['port']
|
||||
return dict(port=result)
|
||||
|
||||
@common.APIFaultWrapper([exception.NetworkNotFound,
|
||||
exception.PortNotFound,
|
||||
exception.StateInvalid])
|
||||
def update(self, request, tenant_id, network_id, id, body):
|
||||
""" Updates the state of a port for a given network """
|
||||
try:
|
||||
request_params = \
|
||||
self._parse_request_params(request, self._port_ops_param_list)
|
||||
except exc.HTTPError as e:
|
||||
return faults.Fault(e)
|
||||
try:
|
||||
self._plugin.update_port(tenant_id, network_id, id,
|
||||
**request_params)
|
||||
return exc.HTTPNoContent()
|
||||
except exception.NetworkNotFound as e:
|
||||
return faults.Fault(faults.NetworkNotFound(e))
|
||||
except exception.PortNotFound as e:
|
||||
return faults.Fault(faults.PortNotFound(e))
|
||||
except exception.StateInvalid as e:
|
||||
return faults.Fault(faults.RequestedStateInvalid(e))
|
||||
body = self._prepare_request_body(body, self._port_ops_param_list)
|
||||
self._plugin.update_port(tenant_id, network_id, id, **body['port'])
|
||||
|
||||
@common.APIFaultWrapper([exception.NetworkNotFound,
|
||||
exception.PortNotFound,
|
||||
exception.PortInUse])
|
||||
def delete(self, request, tenant_id, network_id, id):
|
||||
""" Destroys the port with the given id """
|
||||
#look for port state in request
|
||||
try:
|
||||
self._plugin.delete_port(tenant_id, network_id, id)
|
||||
return exc.HTTPNoContent()
|
||||
except exception.NetworkNotFound as e:
|
||||
return faults.Fault(faults.NetworkNotFound(e))
|
||||
except exception.PortNotFound as e:
|
||||
return faults.Fault(faults.PortNotFound(e))
|
||||
except exception.PortInUse as e:
|
||||
return faults.Fault(faults.PortInUse(e))
|
||||
self._plugin.delete_port(tenant_id, network_id, id)
|
||||
|
||||
|
||||
class ControllerV10(Controller):
|
||||
"""Port resources controller for Quantum v1.0 API"""
|
||||
|
||||
_serialization_metadata = {
|
||||
"attributes": {
|
||||
"port": ["id", "state"],
|
||||
"attachment": ["id"]},
|
||||
"plurals": {"ports": "port"}
|
||||
}
|
||||
|
||||
def __init__(self, plugin):
|
||||
self.version = "1.0"
|
||||
super(ControllerV10, self).__init__(plugin)
|
||||
|
||||
|
||||
class ControllerV11(Controller):
|
||||
"""Port resources controller for Quantum v1.1 API"""
|
||||
|
||||
_serialization_metadata = {
|
||||
"attributes": {
|
||||
"port": ["id", "state"],
|
||||
"attachment": ["id"]},
|
||||
"plurals": {"ports": "port"}
|
||||
}
|
||||
|
||||
def __init__(self, plugin):
|
||||
self.version = "1.1"
|
||||
super(ControllerV11, self).__init__(plugin)
|
||||
|
@ -36,7 +36,7 @@ class Versions(wsgi.Application):
|
||||
},
|
||||
{
|
||||
"id": "v1.1",
|
||||
"status": "FUTURE",
|
||||
"status": "PROPOSED",
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -16,12 +16,16 @@
|
||||
# under the License.
|
||||
|
||||
|
||||
def get_view_builder(req):
|
||||
def get_view_builder(req, version):
|
||||
base_url = req.application_url
|
||||
return ViewBuilder(base_url)
|
||||
view_builder = {
|
||||
'1.0': ViewBuilder10,
|
||||
'1.1': ViewBuilder11,
|
||||
}[version](base_url)
|
||||
return view_builder
|
||||
|
||||
|
||||
class ViewBuilder(object):
|
||||
class ViewBuilder10(object):
|
||||
|
||||
def __init__(self, base_url=None):
|
||||
"""
|
||||
@ -57,3 +61,9 @@ class ViewBuilder(object):
|
||||
if port_data['attachment']:
|
||||
port_dict['attachment'] = dict(id=port_data['attachment'])
|
||||
return port_dict
|
||||
|
||||
|
||||
class ViewBuilder11(ViewBuilder10):
|
||||
#TODO(salvatore-orlando): will extend for Operational status
|
||||
# in appropriate branch
|
||||
pass
|
||||
|
@ -126,6 +126,10 @@ class AlreadyAttachedClient(QuantumException):
|
||||
"already plugged into another port.")
|
||||
|
||||
|
||||
class MalformedRequestBody(QuantumException):
|
||||
message = _("Malformed request body: %(reason)s")
|
||||
|
||||
|
||||
class Duplicate(Error):
|
||||
pass
|
||||
|
||||
|
@ -243,9 +243,11 @@ class QuantumTestResult(result.TextTestResult):
|
||||
if current_case != self._last_case:
|
||||
self.stream.writeln(current_case)
|
||||
self._last_case = current_case
|
||||
|
||||
#NOTE(salvatore-orlando):
|
||||
#slightly changed in order to print test case class
|
||||
#together with unit test name
|
||||
self.stream.write(
|
||||
' %s' % str(test.test._testMethodName).ljust(60))
|
||||
' %s' % str(test.test).ljust(60))
|
||||
self.stream.flush()
|
||||
|
||||
|
||||
|
@ -109,7 +109,7 @@ def _run_wsgi(app_name, paste_conf, paste_config_file):
|
||||
LOG.error(_('No known API applications configured in %s.'),
|
||||
paste_config_file)
|
||||
return
|
||||
server = wsgi.Server()
|
||||
server = wsgi.Server("Quantum")
|
||||
server.start(app,
|
||||
int(paste_conf['bind_port']), paste_conf['bind_host'])
|
||||
return server
|
||||
|
1122
quantum/tests/unit/_test_api.py
Normal file
1122
quantum/tests/unit/_test_api.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -42,7 +42,7 @@ class FakeHTTPConnection:
|
||||
self._req = None
|
||||
plugin = 'quantum.plugins.sample.SamplePlugin.FakePlugin'
|
||||
options = dict(plugin_provider=plugin)
|
||||
self._api = server.APIRouterV1(options)
|
||||
self._api = server.APIRouterV11(options)
|
||||
|
||||
def request(self, method, action, body, headers):
|
||||
# TODO: remove version prefix from action!
|
||||
|
23
quantum/tests/unit/declare_flags.py
Normal file
23
quantum/tests/unit/declare_flags.py
Normal file
@ -0,0 +1,23 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# 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.
|
||||
|
||||
from quantum.common import flags
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
flags.DEFINE_integer('answer', 42, 'test flag')
|
23
quantum/tests/unit/runtime_flags.py
Normal file
23
quantum/tests/unit/runtime_flags.py
Normal file
@ -0,0 +1,23 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# 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.
|
||||
|
||||
from quantum.common import flags
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
flags.DEFINE_integer('runtime_answer', 54, 'test flag')
|
File diff suppressed because it is too large
Load Diff
@ -43,7 +43,8 @@ class CLITest(unittest.TestCase):
|
||||
options = {}
|
||||
options['plugin_provider'] = \
|
||||
'quantum.plugins.sample.SamplePlugin.FakePlugin'
|
||||
self.api = server.APIRouterV1(options)
|
||||
#TODO: make the version of the API router configurable
|
||||
self.api = server.APIRouterV11(options)
|
||||
|
||||
self.tenant_id = "test_tenant"
|
||||
self.network_name_1 = "test_network_1"
|
||||
|
197
quantum/tests/unit/test_flags.py
Normal file
197
quantum/tests/unit/test_flags.py
Normal file
@ -0,0 +1,197 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
# Copyright 2011 Red Hat, Inc.
|
||||
#
|
||||
# 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 gflags
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from quantum.common import flags
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
flags.DEFINE_string('flags_unittest', 'foo', 'for testing purposes only')
|
||||
|
||||
|
||||
class FlagsTestCase(unittest.TestCase):
|
||||
|
||||
def flags(self, **kw):
|
||||
"""Override flag variables for a test."""
|
||||
for k, v in kw.iteritems():
|
||||
setattr(FLAGS, k, v)
|
||||
|
||||
def reset_flags(self):
|
||||
"""Resets all flag variables for the test.
|
||||
|
||||
Runs after each test.
|
||||
|
||||
"""
|
||||
FLAGS.Reset()
|
||||
for k, v in self._original_flags.iteritems():
|
||||
setattr(FLAGS, k, v)
|
||||
|
||||
def setUp(self):
|
||||
super(FlagsTestCase, self).setUp()
|
||||
self.FLAGS = flags.FlagValues()
|
||||
self.global_FLAGS = flags.FLAGS
|
||||
self._original_flags = FLAGS.FlagValuesDict()
|
||||
|
||||
def test_define(self):
|
||||
self.assert_('string' not in self.FLAGS)
|
||||
self.assert_('int' not in self.FLAGS)
|
||||
self.assert_('false' not in self.FLAGS)
|
||||
self.assert_('true' not in self.FLAGS)
|
||||
|
||||
flags.DEFINE_string('string', 'default', 'desc',
|
||||
flag_values=self.FLAGS)
|
||||
flags.DEFINE_integer('int', 1, 'desc', flag_values=self.FLAGS)
|
||||
flags.DEFINE_bool('false', False, 'desc', flag_values=self.FLAGS)
|
||||
flags.DEFINE_bool('true', True, 'desc', flag_values=self.FLAGS)
|
||||
|
||||
self.assert_(self.FLAGS['string'])
|
||||
self.assert_(self.FLAGS['int'])
|
||||
self.assert_(self.FLAGS['false'])
|
||||
self.assert_(self.FLAGS['true'])
|
||||
self.assertEqual(self.FLAGS.string, 'default')
|
||||
self.assertEqual(self.FLAGS.int, 1)
|
||||
self.assertEqual(self.FLAGS.false, False)
|
||||
self.assertEqual(self.FLAGS.true, True)
|
||||
|
||||
argv = ['flags_test',
|
||||
'--string', 'foo',
|
||||
'--int', '2',
|
||||
'--false',
|
||||
'--notrue']
|
||||
|
||||
self.FLAGS(argv)
|
||||
self.assertEqual(self.FLAGS.string, 'foo')
|
||||
self.assertEqual(self.FLAGS.int, 2)
|
||||
self.assertEqual(self.FLAGS.false, True)
|
||||
self.assertEqual(self.FLAGS.true, False)
|
||||
|
||||
def test_define_float(self):
|
||||
flags.DEFINE_float('float', 6.66, 'desc', flag_values=self.FLAGS)
|
||||
self.assertEqual(self.FLAGS.float, 6.66)
|
||||
|
||||
def test_define_multistring(self):
|
||||
flags.DEFINE_multistring('multi', [], 'desc', flag_values=self.FLAGS)
|
||||
|
||||
argv = ['flags_test', '--multi', 'foo', '--multi', 'bar']
|
||||
self.FLAGS(argv)
|
||||
|
||||
self.assertEqual(self.FLAGS.multi, ['foo', 'bar'])
|
||||
|
||||
def test_define_list(self):
|
||||
flags.DEFINE_list('list', ['foo'], 'desc', flag_values=self.FLAGS)
|
||||
|
||||
self.assert_(self.FLAGS['list'])
|
||||
self.assertEqual(self.FLAGS.list, ['foo'])
|
||||
|
||||
argv = ['flags_test', '--list=a,b,c,d']
|
||||
self.FLAGS(argv)
|
||||
|
||||
self.assertEqual(self.FLAGS.list, ['a', 'b', 'c', 'd'])
|
||||
|
||||
def test_error(self):
|
||||
flags.DEFINE_integer('error', 1, 'desc', flag_values=self.FLAGS)
|
||||
|
||||
self.assertEqual(self.FLAGS.error, 1)
|
||||
|
||||
argv = ['flags_test', '--error=foo']
|
||||
self.assertRaises(gflags.IllegalFlagValue, self.FLAGS, argv)
|
||||
|
||||
def test_declare(self):
|
||||
self.assert_('answer' not in self.global_FLAGS)
|
||||
flags.DECLARE('answer', 'quantum.tests.unit.declare_flags')
|
||||
self.assert_('answer' in self.global_FLAGS)
|
||||
self.assertEqual(self.global_FLAGS.answer, 42)
|
||||
|
||||
# Make sure we don't overwrite anything
|
||||
self.global_FLAGS.answer = 256
|
||||
self.assertEqual(self.global_FLAGS.answer, 256)
|
||||
flags.DECLARE('answer', 'quantum.tests.unit.declare_flags')
|
||||
self.assertEqual(self.global_FLAGS.answer, 256)
|
||||
|
||||
def test_getopt_non_interspersed_args(self):
|
||||
self.assert_('runtime_answer' not in self.global_FLAGS)
|
||||
|
||||
argv = ['flags_test', 'extra_arg', '--runtime_answer=60']
|
||||
args = self.global_FLAGS(argv)
|
||||
self.assertEqual(len(args), 3)
|
||||
self.assertEqual(argv, args)
|
||||
|
||||
def test_runtime_and_unknown_flags(self):
|
||||
self.assert_('runtime_answer' not in self.global_FLAGS)
|
||||
|
||||
argv = ['flags_test', '--runtime_answer=60', 'extra_arg']
|
||||
args = self.global_FLAGS(argv)
|
||||
self.assertEqual(len(args), 2)
|
||||
self.assertEqual(args[1], 'extra_arg')
|
||||
|
||||
self.assert_('runtime_answer' not in self.global_FLAGS)
|
||||
|
||||
import quantum.tests.unit.runtime_flags
|
||||
|
||||
self.assert_('runtime_answer' in self.global_FLAGS)
|
||||
self.assertEqual(self.global_FLAGS.runtime_answer, 60)
|
||||
|
||||
def test_flag_overrides(self):
|
||||
self.assertEqual(FLAGS.flags_unittest, 'foo')
|
||||
self.flags(flags_unittest='bar')
|
||||
self.assertEqual(FLAGS.flags_unittest, 'bar')
|
||||
self.assertEqual(FLAGS['flags_unittest'].value, 'bar')
|
||||
self.assertEqual(FLAGS.FlagValuesDict()['flags_unittest'], 'bar')
|
||||
self.reset_flags()
|
||||
self.assertEqual(FLAGS.flags_unittest, 'foo')
|
||||
self.assertEqual(FLAGS['flags_unittest'].value, 'foo')
|
||||
self.assertEqual(FLAGS.FlagValuesDict()['flags_unittest'], 'foo')
|
||||
|
||||
def test_flagfile(self):
|
||||
flags.DEFINE_string('string', 'default', 'desc',
|
||||
flag_values=self.FLAGS)
|
||||
flags.DEFINE_integer('int', 1, 'desc', flag_values=self.FLAGS)
|
||||
flags.DEFINE_bool('false', False, 'desc', flag_values=self.FLAGS)
|
||||
flags.DEFINE_bool('true', True, 'desc', flag_values=self.FLAGS)
|
||||
|
||||
(fd, path) = tempfile.mkstemp(prefix='nova', suffix='.flags')
|
||||
|
||||
try:
|
||||
os.write(fd, '--string=foo\n--int=2\n--false\n--notrue\n')
|
||||
os.close(fd)
|
||||
|
||||
self.FLAGS(['flags_test', '--flagfile=' + path])
|
||||
|
||||
self.assertEqual(self.FLAGS.string, 'foo')
|
||||
self.assertEqual(self.FLAGS.int, 2)
|
||||
self.assertEqual(self.FLAGS.false, True)
|
||||
self.assertEqual(self.FLAGS.true, False)
|
||||
finally:
|
||||
os.remove(path)
|
||||
|
||||
def test_defaults(self):
|
||||
flags.DEFINE_string('foo', 'bar', 'help', flag_values=self.FLAGS)
|
||||
self.assertEqual(self.FLAGS.foo, 'bar')
|
||||
|
||||
self.FLAGS['foo'].SetDefault('blaa')
|
||||
self.assertEqual(self.FLAGS.foo, 'blaa')
|
||||
|
||||
def test_templated_values(self):
|
||||
flags.DEFINE_string('foo', 'foo', 'help', flag_values=self.FLAGS)
|
||||
flags.DEFINE_string('bar', 'bar', 'help', flag_values=self.FLAGS)
|
||||
flags.DEFINE_string('blaa', '$foo$bar', 'help', flag_values=self.FLAGS)
|
||||
self.assertEqual(self.FLAGS.blaa, 'foobar')
|
518
quantum/wsgi.py
518
quantum/wsgi.py
@ -1,7 +1,6 @@
|
||||
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# Copyright 2011, Nicira Networks, Inc.
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@ -27,13 +26,14 @@ eventlet.patcher.monkey_patch(all=False, socket=True)
|
||||
import routes.middleware
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
from xml.dom import minidom
|
||||
|
||||
from lxml import etree
|
||||
from xml.dom import minidom
|
||||
from xml.parsers import expat
|
||||
|
||||
from quantum.common import exceptions as exception
|
||||
from quantum.common import utils
|
||||
|
||||
|
||||
LOG = logging.getLogger('quantum.common.wsgi')
|
||||
|
||||
|
||||
@ -57,8 +57,9 @@ def run_server(application, port):
|
||||
class Server(object):
|
||||
"""Server class to manage multiple WSGI sockets and applications."""
|
||||
|
||||
def __init__(self, threads=1000):
|
||||
def __init__(self, name, threads=1000):
|
||||
self.pool = eventlet.GreenPool(threads)
|
||||
self.name = name
|
||||
|
||||
def start(self, application, port, host='0.0.0.0', backlog=128):
|
||||
"""Run a WSGI server with the given application."""
|
||||
@ -119,12 +120,13 @@ class Request(webob.Request):
|
||||
def best_match_content_type(self):
|
||||
"""Determine the most acceptable content-type.
|
||||
|
||||
Based on the query extension then the Accept header.
|
||||
|
||||
Based on:
|
||||
1) URI extension (.json/.xml)
|
||||
2) Content-type header
|
||||
3) Accept* headers
|
||||
"""
|
||||
# First lookup http request
|
||||
parts = self.path.rsplit('.', 1)
|
||||
LOG.debug("Request parts:%s", parts)
|
||||
if len(parts) > 1:
|
||||
format = parts[1]
|
||||
if format in ['json', 'xml']:
|
||||
@ -148,10 +150,379 @@ class Request(webob.Request):
|
||||
type = self.content_type
|
||||
if type in allowed_types:
|
||||
return type
|
||||
LOG.debug(_("Wrong Content-Type: %s") % type)
|
||||
return None
|
||||
|
||||
|
||||
class ActionDispatcher(object):
|
||||
"""Maps method name to local methods through action name."""
|
||||
|
||||
def dispatch(self, *args, **kwargs):
|
||||
"""Find and call local method."""
|
||||
action = kwargs.pop('action', 'default')
|
||||
action_method = getattr(self, str(action), self.default)
|
||||
return action_method(*args, **kwargs)
|
||||
|
||||
def default(self, data):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class DictSerializer(ActionDispatcher):
|
||||
"""Default request body serialization"""
|
||||
|
||||
def serialize(self, data, action='default'):
|
||||
return self.dispatch(data, action=action)
|
||||
|
||||
def default(self, data):
|
||||
return ""
|
||||
|
||||
|
||||
class JSONDictSerializer(DictSerializer):
|
||||
"""Default JSON request body serialization"""
|
||||
|
||||
def default(self, data):
|
||||
return utils.dumps(data)
|
||||
|
||||
|
||||
class XMLDictSerializer(DictSerializer):
|
||||
|
||||
def __init__(self, metadata=None, xmlns=None):
|
||||
"""
|
||||
:param metadata: information needed to deserialize xml into
|
||||
a dictionary.
|
||||
:param xmlns: XML namespace to include with serialized xml
|
||||
"""
|
||||
super(XMLDictSerializer, self).__init__()
|
||||
self.metadata = metadata or {}
|
||||
self.xmlns = xmlns
|
||||
|
||||
def default(self, data):
|
||||
# We expect data to contain a single key which is the XML root.
|
||||
root_key = data.keys()[0]
|
||||
doc = minidom.Document()
|
||||
node = self._to_xml_node(doc, self.metadata, root_key, data[root_key])
|
||||
|
||||
return self.to_xml_string(node)
|
||||
|
||||
def to_xml_string(self, node, has_atom=False):
|
||||
self._add_xmlns(node, has_atom)
|
||||
return node.toxml('UTF-8')
|
||||
|
||||
#NOTE (ameade): the has_atom should be removed after all of the
|
||||
# xml serializers and view builders have been updated to the current
|
||||
# spec that required all responses include the xmlns:atom, the has_atom
|
||||
# flag is to prevent current tests from breaking
|
||||
def _add_xmlns(self, node, has_atom=False):
|
||||
if self.xmlns is not None:
|
||||
node.setAttribute('xmlns', self.xmlns)
|
||||
if has_atom:
|
||||
node.setAttribute('xmlns:atom', "http://www.w3.org/2005/Atom")
|
||||
|
||||
def _to_xml_node(self, doc, metadata, nodename, data):
|
||||
"""Recursive method to convert data members to XML nodes."""
|
||||
result = doc.createElement(nodename)
|
||||
|
||||
# Set the xml namespace if one is specified
|
||||
# TODO(justinsb): We could also use prefixes on the keys
|
||||
xmlns = metadata.get('xmlns', None)
|
||||
if xmlns:
|
||||
result.setAttribute('xmlns', xmlns)
|
||||
|
||||
#TODO(bcwaldon): accomplish this without a type-check
|
||||
if type(data) is list:
|
||||
collections = metadata.get('list_collections', {})
|
||||
if nodename in collections:
|
||||
metadata = collections[nodename]
|
||||
for item in data:
|
||||
node = doc.createElement(metadata['item_name'])
|
||||
node.setAttribute(metadata['item_key'], str(item))
|
||||
result.appendChild(node)
|
||||
return result
|
||||
singular = metadata.get('plurals', {}).get(nodename, None)
|
||||
if singular is None:
|
||||
if nodename.endswith('s'):
|
||||
singular = nodename[:-1]
|
||||
else:
|
||||
singular = 'item'
|
||||
for item in data:
|
||||
node = self._to_xml_node(doc, metadata, singular, item)
|
||||
result.appendChild(node)
|
||||
#TODO(bcwaldon): accomplish this without a type-check
|
||||
elif type(data) is dict:
|
||||
collections = metadata.get('dict_collections', {})
|
||||
if nodename in collections:
|
||||
metadata = collections[nodename]
|
||||
for k, v in data.items():
|
||||
node = doc.createElement(metadata['item_name'])
|
||||
node.setAttribute(metadata['item_key'], str(k))
|
||||
text = doc.createTextNode(str(v))
|
||||
node.appendChild(text)
|
||||
result.appendChild(node)
|
||||
return result
|
||||
attrs = metadata.get('attributes', {}).get(nodename, {})
|
||||
for k, v in data.items():
|
||||
if k in attrs:
|
||||
result.setAttribute(k, str(v))
|
||||
else:
|
||||
node = self._to_xml_node(doc, metadata, k, v)
|
||||
result.appendChild(node)
|
||||
else:
|
||||
# Type is atom
|
||||
node = doc.createTextNode(str(data))
|
||||
result.appendChild(node)
|
||||
return result
|
||||
|
||||
def _create_link_nodes(self, xml_doc, links):
|
||||
link_nodes = []
|
||||
for link in links:
|
||||
link_node = xml_doc.createElement('atom:link')
|
||||
link_node.setAttribute('rel', link['rel'])
|
||||
link_node.setAttribute('href', link['href'])
|
||||
if 'type' in link:
|
||||
link_node.setAttribute('type', link['type'])
|
||||
link_nodes.append(link_node)
|
||||
return link_nodes
|
||||
|
||||
def _to_xml(self, root):
|
||||
"""Convert the xml object to an xml string."""
|
||||
return etree.tostring(root, encoding='UTF-8', xml_declaration=True)
|
||||
|
||||
|
||||
class ResponseHeaderSerializer(ActionDispatcher):
|
||||
"""Default response headers serialization"""
|
||||
|
||||
def serialize(self, response, data, action):
|
||||
self.dispatch(response, data, action=action)
|
||||
|
||||
def default(self, response, data):
|
||||
response.status_int = 200
|
||||
|
||||
|
||||
class ResponseSerializer(object):
|
||||
"""Encode the necessary pieces into a response object"""
|
||||
|
||||
def __init__(self, body_serializers=None, headers_serializer=None):
|
||||
self.body_serializers = {
|
||||
'application/xml': XMLDictSerializer(),
|
||||
'application/json': JSONDictSerializer(),
|
||||
}
|
||||
self.body_serializers.update(body_serializers or {})
|
||||
|
||||
self.headers_serializer = headers_serializer or \
|
||||
ResponseHeadersSerializer()
|
||||
|
||||
def serialize(self, response_data, content_type, action='default'):
|
||||
"""Serialize a dict into a string and wrap in a wsgi.Request object.
|
||||
|
||||
:param response_data: dict produced by the Controller
|
||||
:param content_type: expected mimetype of serialized response body
|
||||
|
||||
"""
|
||||
response = webob.Response()
|
||||
self.serialize_headers(response, response_data, action)
|
||||
self.serialize_body(response, response_data, content_type, action)
|
||||
return response
|
||||
|
||||
def serialize_headers(self, response, data, action):
|
||||
self.headers_serializer.serialize(response, data, action)
|
||||
|
||||
def serialize_body(self, response, data, content_type, action):
|
||||
response.headers['Content-Type'] = content_type
|
||||
if data is not None:
|
||||
serializer = self.get_body_serializer(content_type)
|
||||
response.body = serializer.serialize(data, action)
|
||||
|
||||
def get_body_serializer(self, content_type):
|
||||
try:
|
||||
return self.body_serializers[content_type]
|
||||
except (KeyError, TypeError):
|
||||
raise exception.InvalidContentType(content_type=content_type)
|
||||
|
||||
|
||||
class TextDeserializer(ActionDispatcher):
|
||||
"""Default request body deserialization"""
|
||||
|
||||
def deserialize(self, datastring, action='default'):
|
||||
return self.dispatch(datastring, action=action)
|
||||
|
||||
def default(self, datastring):
|
||||
return {}
|
||||
|
||||
|
||||
class JSONDeserializer(TextDeserializer):
|
||||
|
||||
def _from_json(self, datastring):
|
||||
try:
|
||||
return utils.loads(datastring)
|
||||
except ValueError:
|
||||
msg = _("cannot understand JSON")
|
||||
raise exception.MalformedRequestBody(reason=msg)
|
||||
|
||||
def default(self, datastring):
|
||||
return {'body': self._from_json(datastring)}
|
||||
|
||||
|
||||
class XMLDeserializer(TextDeserializer):
|
||||
|
||||
def __init__(self, metadata=None):
|
||||
"""
|
||||
:param metadata: information needed to deserialize xml into
|
||||
a dictionary.
|
||||
"""
|
||||
super(XMLDeserializer, self).__init__()
|
||||
self.metadata = metadata or {}
|
||||
|
||||
def _from_xml(self, datastring):
|
||||
plurals = set(self.metadata.get('plurals', {}))
|
||||
try:
|
||||
node = minidom.parseString(datastring).childNodes[0]
|
||||
return {node.nodeName: self._from_xml_node(node, plurals)}
|
||||
except expat.ExpatError:
|
||||
msg = _("cannot understand XML")
|
||||
raise exception.MalformedRequestBody(reason=msg)
|
||||
|
||||
def _from_xml_node(self, node, listnames):
|
||||
"""Convert a minidom node to a simple Python type.
|
||||
|
||||
:param listnames: list of XML node names whose subnodes should
|
||||
be considered list items.
|
||||
|
||||
"""
|
||||
if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3:
|
||||
return node.childNodes[0].nodeValue
|
||||
elif node.nodeName in listnames:
|
||||
return [self._from_xml_node(n, listnames) for n in node.childNodes]
|
||||
else:
|
||||
result = dict()
|
||||
for attr in node.attributes.keys():
|
||||
result[attr] = node.attributes[attr].nodeValue
|
||||
for child in node.childNodes:
|
||||
if child.nodeType != node.TEXT_NODE:
|
||||
result[child.nodeName] = self._from_xml_node(child,
|
||||
listnames)
|
||||
return result
|
||||
|
||||
def find_first_child_named(self, parent, name):
|
||||
"""Search a nodes children for the first child with a given name"""
|
||||
for node in parent.childNodes:
|
||||
if node.nodeName == name:
|
||||
return node
|
||||
return None
|
||||
|
||||
def find_children_named(self, parent, name):
|
||||
"""Return all of a nodes children who have the given name"""
|
||||
for node in parent.childNodes:
|
||||
if node.nodeName == name:
|
||||
yield node
|
||||
|
||||
def extract_text(self, node):
|
||||
"""Get the text field contained by the given node"""
|
||||
if len(node.childNodes) == 1:
|
||||
child = node.childNodes[0]
|
||||
if child.nodeType == child.TEXT_NODE:
|
||||
return child.nodeValue
|
||||
return ""
|
||||
|
||||
def default(self, datastring):
|
||||
return {'body': self._from_xml(datastring)}
|
||||
|
||||
|
||||
class RequestHeadersDeserializer(ActionDispatcher):
|
||||
"""Default request headers deserializer"""
|
||||
|
||||
def deserialize(self, request, action):
|
||||
return self.dispatch(request, action=action)
|
||||
|
||||
def default(self, request):
|
||||
return {}
|
||||
|
||||
|
||||
class RequestDeserializer(object):
|
||||
"""Break up a Request object into more useful pieces."""
|
||||
|
||||
def __init__(self, body_deserializers=None, headers_deserializer=None):
|
||||
self.body_deserializers = {
|
||||
'application/xml': XMLDeserializer(),
|
||||
'application/json': JSONDeserializer(),
|
||||
}
|
||||
self.body_deserializers.update(body_deserializers or {})
|
||||
|
||||
self.headers_deserializer = headers_deserializer or \
|
||||
RequestHeadersDeserializer()
|
||||
|
||||
def deserialize(self, request):
|
||||
"""Extract necessary pieces of the request.
|
||||
|
||||
:param request: Request object
|
||||
:returns tuple of expected controller action name, dictionary of
|
||||
keyword arguments to pass to the controller, the expected
|
||||
content type of the response
|
||||
|
||||
"""
|
||||
action_args = self.get_action_args(request.environ)
|
||||
action = action_args.pop('action', None)
|
||||
|
||||
action_args.update(self.deserialize_headers(request, action))
|
||||
action_args.update(self.deserialize_body(request, action))
|
||||
|
||||
accept = self.get_expected_content_type(request)
|
||||
|
||||
return (action, action_args, accept)
|
||||
|
||||
def deserialize_headers(self, request, action):
|
||||
return self.headers_deserializer.deserialize(request, action)
|
||||
|
||||
def deserialize_body(self, request, action):
|
||||
try:
|
||||
content_type = request.best_match_content_type()
|
||||
except exception.InvalidContentType:
|
||||
LOG.debug(_("Unrecognized Content-Type provided in request"))
|
||||
return {}
|
||||
|
||||
if content_type is None:
|
||||
LOG.debug(_("No Content-Type provided in request"))
|
||||
return {}
|
||||
|
||||
if not len(request.body) > 0:
|
||||
LOG.debug(_("Empty body provided in request"))
|
||||
return {}
|
||||
|
||||
try:
|
||||
deserializer = self.get_body_deserializer(content_type)
|
||||
except exception.InvalidContentType:
|
||||
LOG.debug(_("Unable to deserialize body as provided Content-Type"))
|
||||
raise
|
||||
|
||||
return deserializer.deserialize(request.body, action)
|
||||
|
||||
def get_body_deserializer(self, content_type):
|
||||
try:
|
||||
return self.body_deserializers[content_type]
|
||||
except (KeyError, TypeError):
|
||||
raise exception.InvalidContentType(content_type=content_type)
|
||||
|
||||
def get_expected_content_type(self, request):
|
||||
return request.best_match_content_type()
|
||||
|
||||
def get_action_args(self, request_environment):
|
||||
"""Parse dictionary created by routes library."""
|
||||
try:
|
||||
args = request_environment['wsgiorg.routing_args'][1].copy()
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
try:
|
||||
del args['controller']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
del args['format']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return args
|
||||
|
||||
|
||||
class Application(object):
|
||||
"""Base WSGI application wrapper. Subclasses need to implement __call__."""
|
||||
|
||||
@ -317,6 +688,131 @@ class Router(object):
|
||||
return app
|
||||
|
||||
|
||||
class Resource(Application):
|
||||
"""WSGI app that handles (de)serialization and controller dispatch.
|
||||
|
||||
WSGI app that reads routing information supplied by RoutesMiddleware
|
||||
and calls the requested action method upon its controller. All
|
||||
controller action methods must accept a 'req' argument, which is the
|
||||
incoming wsgi.Request. If the operation is a PUT or POST, the controller
|
||||
method must also accept a 'body' argument (the deserialized request body).
|
||||
They may raise a webob.exc exception or return a dict, which will be
|
||||
serialized by requested content type.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, controller, 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
|
||||
|
||||
"""
|
||||
self.controller = controller
|
||||
self.deserializer = deserializer or RequestDeserializer()
|
||||
self.serializer = serializer or ResponseSerializer()
|
||||
# use serializer's xmlns for populating Fault generator xmlns
|
||||
xml_serializer = self.serializer.body_serializers['application/xml']
|
||||
if hasattr(xml_serializer, 'xmlns'):
|
||||
self._xmlns = xml_serializer.xmlns
|
||||
|
||||
@webob.dec.wsgify(RequestClass=Request)
|
||||
def __call__(self, request):
|
||||
"""WSGI method that controls (de)serialization and method dispatch."""
|
||||
|
||||
LOG.info("%(method)s %(url)s" % {"method": request.method,
|
||||
"url": request.url})
|
||||
|
||||
try:
|
||||
action, args, accept = self.deserializer.deserialize(request)
|
||||
except exception.InvalidContentType:
|
||||
msg = _("Unsupported Content-Type")
|
||||
LOG.exception("InvalidContentType:%s", msg)
|
||||
return Fault(webob.exc.HTTPBadRequest(explanation=msg),
|
||||
self._xmlns)
|
||||
except exception.MalformedRequestBody:
|
||||
msg = _("Malformed request body")
|
||||
LOG.exception("MalformedRequestBody:%s", msg)
|
||||
return Fault(webob.exc.HTTPBadRequest(explanation=msg),
|
||||
self._xmlns)
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
if type(action_result) is dict or action_result is None:
|
||||
response = self.serializer.serialize(action_result,
|
||||
accept,
|
||||
action=action)
|
||||
else:
|
||||
response = action_result
|
||||
|
||||
try:
|
||||
msg_dict = dict(url=request.url, status=response.status_int)
|
||||
msg = _("%(url)s returned with HTTP %(status)d") % msg_dict
|
||||
except AttributeError, e:
|
||||
msg_dict = dict(url=request.url, e=e)
|
||||
msg = _("%(url)s returned a fault: %(e)s" % msg_dict)
|
||||
|
||||
LOG.info(msg)
|
||||
|
||||
return response
|
||||
|
||||
def dispatch(self, request, action, action_args):
|
||||
"""Find action-spefic method on controller and call it."""
|
||||
|
||||
controller_method = getattr(self.controller, action)
|
||||
try:
|
||||
#NOTE(salvatore-orlando): the controller method must have
|
||||
# an argument whose name is 'request'
|
||||
return controller_method(request=request, **action_args)
|
||||
except TypeError as exc:
|
||||
LOG.exception(exc)
|
||||
return Fault(webob.exc.HTTPBadRequest(),
|
||||
self._xmlns)
|
||||
|
||||
|
||||
class Fault(webob.exc.HTTPException):
|
||||
""" Generates an HTTP response from a webob HTTP exception"""
|
||||
|
||||
def __init__(self, exception, xmlns=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
|
||||
|
||||
@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'}}}
|
||||
xml_serializer = XMLDictSerializer(metadata, self._xmlns)
|
||||
content_type = req.best_match_content_type()
|
||||
serializer = {
|
||||
'application/xml': xml_serializer,
|
||||
'application/json': JSONDictSerializer(),
|
||||
}[content_type]
|
||||
|
||||
self.wrapped_exc.body = serializer.serialize(fault_data)
|
||||
self.wrapped_exc.content_type = content_type
|
||||
return self.wrapped_exc
|
||||
|
||||
|
||||
# NOTE(salvatore-orlando): this class will go once the
|
||||
# extension API framework is updated
|
||||
class Controller(object):
|
||||
"""WSGI app that dispatched to methods.
|
||||
|
||||
@ -389,6 +885,8 @@ class Controller(object):
|
||||
return None
|
||||
|
||||
|
||||
# NOTE(salvatore-orlando): this class will go once the
|
||||
# extension API framework is updated
|
||||
class Serializer(object):
|
||||
"""Serializes and deserializes dictionaries to certain MIME types."""
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user