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:
Ghe Rivero 2011-12-01 01:14:29 +00:00 committed by Salvatore Orlando
parent 2184460b65
commit f53fb7705a
20 changed files with 2311 additions and 1432 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -36,7 +36,7 @@ class Versions(wsgi.Application):
},
{
"id": "v1.1",
"status": "FUTURE",
"status": "PROPOSED",
},
]

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View 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')

View 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

View File

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

View 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')

View File

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