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