Add API v2 support
* Implements BP v2-api-melange-integration * Adds v2 Plugin specification * Refactors SQLAlchemy usage for multiple BASE's Change-Id: I45f008f181c18269afdfe4a9b589a7c5ae56d225
This commit is contained in:
parent
a164532cab
commit
73f41d370e
@ -23,6 +23,7 @@ use = egg:Paste#urlmap
|
||||
/: quantumversions
|
||||
/v1.0: quantumapi_v1_0
|
||||
/v1.1: quantumapi_v1_1
|
||||
/v2.0: quantumapi_v2_0
|
||||
|
||||
[pipeline:quantumapi_v1_0]
|
||||
# By default, authentication is disabled.
|
||||
@ -38,6 +39,13 @@ pipeline = extensions quantumapiapp_v1_0
|
||||
pipeline = extensions quantumapiapp_v1_1
|
||||
# pipeline = authtoken keystonecontext extensions quantumapiapp_v1_1
|
||||
|
||||
[pipeline:quantumapi_v2_0]
|
||||
# By default, authentication is disabled.
|
||||
# To enable Keystone integration comment out the
|
||||
# following line and uncomment the next one
|
||||
pipeline = extensions quantumapiapp_v2_0
|
||||
# pipeline = authtoken keystonecontext extensions quantumapiapp_v2_0
|
||||
|
||||
[filter:keystonecontext]
|
||||
paste.filter_factory = quantum.auth:QuantumKeystoneContext.factory
|
||||
|
||||
@ -61,3 +69,6 @@ paste.app_factory = quantum.api:APIRouterV10.factory
|
||||
|
||||
[app:quantumapiapp_v1_1]
|
||||
paste.app_factory = quantum.api:APIRouterV11.factory
|
||||
|
||||
[app:quantumapiapp_v2_0]
|
||||
paste.app_factory = quantum.api.v2.router:APIRouter.factory
|
||||
|
14
quantum/api/v2/__init__.py
Normal file
14
quantum/api/v2/__init__.py
Normal file
@ -0,0 +1,14 @@
|
||||
# Copyright (c) 2012 OpenStack, LLC.
|
||||
#
|
||||
# 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.
|
208
quantum/api/v2/base.py
Normal file
208
quantum/api/v2/base.py
Normal file
@ -0,0 +1,208 @@
|
||||
# Copyright (c) 2012 OpenStack, LLC.
|
||||
#
|
||||
# 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 logging
|
||||
|
||||
import webob.exc
|
||||
|
||||
from quantum.common import exceptions
|
||||
from quantum.api.v2 import resource as wsgi_resource
|
||||
from quantum.common import utils
|
||||
from quantum.api.v2 import views
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
XML_NS_V20 = 'http://openstack.org/quantum/api/v2.0'
|
||||
|
||||
FAULT_MAP = {exceptions.NotFound: webob.exc.HTTPNotFound,
|
||||
exceptions.InUse: webob.exc.HTTPConflict,
|
||||
exceptions.StateInvalid: webob.exc.HTTPBadRequest}
|
||||
|
||||
|
||||
def fields(request):
|
||||
"""
|
||||
Extracts the list of fields to return
|
||||
"""
|
||||
return [v for v in request.GET.getall('fields') if v]
|
||||
|
||||
|
||||
def filters(request):
|
||||
"""
|
||||
Extracts the filters from the request string
|
||||
|
||||
Returns a dict of lists for the filters:
|
||||
|
||||
check=a&check=b&name=Bob&verbose=True&verbose=other
|
||||
|
||||
becomes
|
||||
|
||||
{'check': [u'a', u'b'], 'name': [u'Bob']}
|
||||
"""
|
||||
res = {}
|
||||
for key in set(request.GET):
|
||||
if key in ('verbose', 'fields'):
|
||||
continue
|
||||
|
||||
values = [v for v in request.GET.getall(key) if v]
|
||||
if values:
|
||||
res[key] = values
|
||||
return res
|
||||
|
||||
|
||||
def verbose(request):
|
||||
"""
|
||||
Determines the verbose fields for a request
|
||||
|
||||
Returns a list of items that are requested to be verbose:
|
||||
|
||||
check=a&check=b&name=Bob&verbose=True&verbose=other
|
||||
|
||||
returns
|
||||
|
||||
[True]
|
||||
|
||||
and
|
||||
|
||||
check=a&check=b&name=Bob&verbose=other
|
||||
|
||||
returns
|
||||
|
||||
['other']
|
||||
|
||||
"""
|
||||
verbose = [utils.boolize(v) for v in request.GET.getall('verbose') if v]
|
||||
|
||||
# NOTE(jkoelker) verbose=<bool> trumps all other verbose settings
|
||||
if True in verbose:
|
||||
return True
|
||||
elif False in verbose:
|
||||
return False
|
||||
|
||||
return verbose
|
||||
|
||||
|
||||
class Controller(object):
|
||||
def __init__(self, plugin, collection, resource, params):
|
||||
self._plugin = plugin
|
||||
self._collection = collection
|
||||
self._resource = resource
|
||||
self._params = params
|
||||
self._view = getattr(views, self._resource)
|
||||
|
||||
def _items(self, request):
|
||||
"""Retrieves and formats a list of elements of the requested entity"""
|
||||
kwargs = {'filters': filters(request),
|
||||
'verbose': verbose(request),
|
||||
'fields': fields(request)}
|
||||
|
||||
obj_getter = getattr(self._plugin, "get_%s" % self._collection)
|
||||
obj_list = obj_getter(request.context, **kwargs)
|
||||
return {self._collection: [self._view(obj) for obj in obj_list]}
|
||||
|
||||
def _item(self, request, id):
|
||||
"""Retrieves and formats a single element of the requested entity"""
|
||||
kwargs = {'verbose': verbose(request),
|
||||
'fields': fields(request)}
|
||||
obj_getter = getattr(self._plugin,
|
||||
"get_%s" % self._resource)
|
||||
obj = obj_getter(request.context, id, **kwargs)
|
||||
return {self._resource: self._view(obj)}
|
||||
|
||||
def index(self, request):
|
||||
"""Returns a list of the requested entity"""
|
||||
return self._items(request)
|
||||
|
||||
def show(self, request, id):
|
||||
"""Returns detailed information about the requested entity"""
|
||||
return self._item(request, id)
|
||||
|
||||
def create(self, request, body=None):
|
||||
"""Creates a new instance of the requested entity"""
|
||||
body = self._prepare_request_body(body, allow_bulk=True)
|
||||
obj_creator = getattr(self._plugin,
|
||||
"create_%s" % self._resource)
|
||||
kwargs = {self._resource: body}
|
||||
obj = obj_creator(request.context, **kwargs)
|
||||
return {self._resource: self._view(obj)}
|
||||
|
||||
def delete(self, request, id):
|
||||
"""Deletes the specified entity"""
|
||||
obj_deleter = getattr(self._plugin,
|
||||
"delete_%s" % self._resource)
|
||||
obj_deleter(request.context, id)
|
||||
|
||||
def update(self, request, id, body=None):
|
||||
"""Updates the specified entity's attributes"""
|
||||
obj_updater = getattr(self._plugin,
|
||||
"update_%s" % self._resource)
|
||||
kwargs = {self._resource: body}
|
||||
obj = obj_updater(request.context, id, **kwargs)
|
||||
return {self._resource: self._view(obj)}
|
||||
|
||||
def _prepare_request_body(self, body, allow_bulk=False):
|
||||
""" verifies required parameters are in request body.
|
||||
Parameters with default values are considered to be
|
||||
optional.
|
||||
|
||||
body argument must be the deserialized body
|
||||
"""
|
||||
if not body:
|
||||
raise webob.exc.HTTPBadRequest(_("Resource body required"))
|
||||
|
||||
body = body or {self._resource: {}}
|
||||
|
||||
if self._collection in body and allow_bulk:
|
||||
bulk_body = [self._prepare_request_body({self._resource: b})
|
||||
if self._resource not in b
|
||||
else self._prepare_request_body(b)
|
||||
for b in body[self._collection]]
|
||||
|
||||
if not bulk_body:
|
||||
raise webob.exc.HTTPBadRequest(_("Resources required"))
|
||||
|
||||
return {self._collection: bulk_body}
|
||||
|
||||
elif self._collection in body and not allow_bulk:
|
||||
raise webob.exc.HTTPBadRequest("Bulk operation not supported")
|
||||
|
||||
res_dict = body.get(self._resource)
|
||||
if res_dict is None:
|
||||
msg = _("Unable to find '%s' in request body") % self._resource
|
||||
raise webob.exc.HTTPBadRequest(msg)
|
||||
|
||||
for param in self._params:
|
||||
param_value = res_dict.get(param['attr'], param.get('default'))
|
||||
if param_value is None:
|
||||
msg = _("Failed to parse request. Parameter %s not "
|
||||
"specified") % param
|
||||
raise webob.exc.HTTPUnprocessableEntity(msg)
|
||||
res_dict[param['attr']] = param_value
|
||||
return body
|
||||
|
||||
|
||||
def create_resource(collection, resource, plugin, conf, params):
|
||||
controller = Controller(plugin, collection, resource, params)
|
||||
|
||||
# NOTE(jkoelker) To anyone wishing to add "proper" xml support
|
||||
# this is where you do it
|
||||
serializers = {
|
||||
# 'application/xml': wsgi.XMLDictSerializer(metadata, XML_NS_V20),
|
||||
}
|
||||
|
||||
deserializers = {
|
||||
# 'application/xml': wsgi.XMLDeserializer(metadata),
|
||||
}
|
||||
|
||||
return wsgi_resource.Resource(controller, FAULT_MAP, deserializers,
|
||||
serializers)
|
126
quantum/api/v2/resource.py
Normal file
126
quantum/api/v2/resource.py
Normal file
@ -0,0 +1,126 @@
|
||||
# Copyright 2012 OpenStack LLC.
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Utility methods for working with WSGI servers redux
|
||||
"""
|
||||
import logging
|
||||
|
||||
import webob
|
||||
import webob.exc
|
||||
import webob.dec
|
||||
|
||||
from quantum import context
|
||||
from quantum.common import exceptions
|
||||
from quantum.openstack.common import jsonutils as json
|
||||
from quantum import wsgi
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Request(webob.Request):
|
||||
"""Add some Openstack API-specific logic to the base webob.Request."""
|
||||
|
||||
def best_match_content_type(self):
|
||||
supported = ('application/json', )
|
||||
return self.accept.best_match(supported,
|
||||
default_match='application/json')
|
||||
|
||||
@property
|
||||
def context(self):
|
||||
#Eventually the Auth[NZ] code will supply this. (mdragon)
|
||||
#when that happens this if block should raise instead.
|
||||
if 'quantum.context' not in self.environ:
|
||||
self.environ['quantum.context'] = context.get_admin_context()
|
||||
return self.environ['quantum.context']
|
||||
|
||||
|
||||
def Resource(controller, faults=None, deserializers=None, serializers=None):
|
||||
"""Represents an API entity resource and the associated serialization and
|
||||
deserialization logic
|
||||
"""
|
||||
default_deserializers = {'application/xml': wsgi.XMLDeserializer(),
|
||||
'application/json': lambda x: json.loads(x)}
|
||||
default_serializers = {'application/xml': wsgi.XMLDictSerializer(),
|
||||
'application/json': lambda x: json.dumps(x)}
|
||||
format_types = {'xml': 'application/xml',
|
||||
'json': 'application/json'}
|
||||
action_status = dict(create=201, delete=204)
|
||||
|
||||
default_deserializers.update(deserializers or {})
|
||||
default_serializers.update(serializers or {})
|
||||
|
||||
deserializers = default_deserializers
|
||||
serializers = default_serializers
|
||||
faults = faults or {}
|
||||
|
||||
@webob.dec.wsgify(RequestClass=Request)
|
||||
def resource(request):
|
||||
route_args = request.environ.get('wsgiorg.routing_args')
|
||||
if route_args:
|
||||
args = route_args[1].copy()
|
||||
else:
|
||||
args = {}
|
||||
|
||||
# NOTE(jkoelker) by now the controller is already found, remove
|
||||
# it from the args if it is in the matchdict
|
||||
args.pop('controller', None)
|
||||
fmt = args.pop('format', None)
|
||||
action = args.pop('action', None)
|
||||
|
||||
content_type = format_types.get(fmt,
|
||||
request.best_match_content_type())
|
||||
deserializer = deserializers.get(content_type)
|
||||
serializer = serializers.get(content_type)
|
||||
|
||||
try:
|
||||
if request.body:
|
||||
args['body'] = deserializer(request.body)
|
||||
|
||||
method = getattr(controller, action)
|
||||
|
||||
result = method(request=request, **args)
|
||||
except exceptions.QuantumException as e:
|
||||
LOG.exception('%s failed' % action)
|
||||
body = serializer({'QuantumError': str(e)})
|
||||
kwargs = {'body': body, 'content_type': content_type}
|
||||
for fault in faults:
|
||||
if isinstance(e, fault):
|
||||
raise faults[fault](**kwargs)
|
||||
raise webob.exc.HTTPInternalServerError(**kwargs)
|
||||
except webob.exc.HTTPException as e:
|
||||
LOG.exception('%s failed' % action)
|
||||
e.body = serializer({'QuantumError': str(e)})
|
||||
e.content_type = content_type
|
||||
raise
|
||||
except Exception as e:
|
||||
# NOTE(jkoelker) Everyting else is 500
|
||||
LOG.exception('%s failed' % action)
|
||||
body = serializer({'QuantumError': str(e)})
|
||||
kwargs = {'body': body, 'content_type': content_type}
|
||||
raise webob.exc.HTTPInternalServerError(**kwargs)
|
||||
|
||||
status = action_status.get(action, 200)
|
||||
body = serializer(result)
|
||||
# NOTE(jkoelker) Comply with RFC2616 section 9.7
|
||||
if status == 204:
|
||||
content_type = ''
|
||||
body = None
|
||||
|
||||
return webob.Response(request=request, status=status,
|
||||
content_type=content_type,
|
||||
body=body)
|
||||
return resource
|
119
quantum/api/v2/router.py
Normal file
119
quantum/api/v2/router.py
Normal file
@ -0,0 +1,119 @@
|
||||
# Copyright (c) 2012 OpenStack, LLC.
|
||||
#
|
||||
# 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 logging
|
||||
import urlparse
|
||||
|
||||
import routes as routes_mapper
|
||||
import webob
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
|
||||
from quantum import manager
|
||||
from quantum import wsgi
|
||||
from quantum.api.v2 import base
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
HEX_ELEM = '[0-9A-Fa-f]'
|
||||
UUID_PATTERN = '-'.join([HEX_ELEM + '{8}', HEX_ELEM + '{4}',
|
||||
HEX_ELEM + '{4}', HEX_ELEM + '{4}',
|
||||
HEX_ELEM + '{12}'])
|
||||
COLLECTION_ACTIONS = ['index', 'create']
|
||||
MEMBER_ACTIONS = ['show', 'update', 'delete']
|
||||
REQUIREMENTS = {'id': UUID_PATTERN, 'format': 'xml|json'}
|
||||
|
||||
|
||||
RESOURCE_PARAM_MAP = {
|
||||
'networks': [
|
||||
{'attr': 'name'},
|
||||
],
|
||||
'ports': [
|
||||
{'attr': 'state', 'default': 'DOWN'},
|
||||
],
|
||||
'subnets': [
|
||||
{'attr': 'prefix'},
|
||||
{'attr': 'network_id'},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class Index(wsgi.Application):
|
||||
def __init__(self, resources):
|
||||
self.resources = resources
|
||||
|
||||
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
||||
def __call__(self, req):
|
||||
metadata = {'application/xml': {
|
||||
'attributes': {
|
||||
'resource': ['name', 'collection'],
|
||||
'link': ['href', 'rel'],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layout = []
|
||||
for name, collection in self.resources.iteritems():
|
||||
href = urlparse.urljoin(req.path_url, collection)
|
||||
resource = {'name': name,
|
||||
'collection': collection,
|
||||
'links': [{'rel': 'self',
|
||||
'href': href}]}
|
||||
layout.append(resource)
|
||||
response = dict(resources=layout)
|
||||
content_type = req.best_match_content_type()
|
||||
body = wsgi.Serializer(metadata=metadata).serialize(response,
|
||||
content_type)
|
||||
return webob.Response(body=body, content_type=content_type)
|
||||
|
||||
|
||||
class APIRouter(wsgi.Router):
|
||||
|
||||
@classmethod
|
||||
def factory(cls, global_config, **local_config):
|
||||
return cls(global_config, **local_config)
|
||||
|
||||
def __init__(self, conf, **local_config):
|
||||
mapper = routes_mapper.Mapper()
|
||||
plugin_provider = manager.get_plugin_provider(conf)
|
||||
plugin = manager.get_plugin(plugin_provider)
|
||||
|
||||
# NOTE(jkoelker) Merge local_conf into conf after the plugin
|
||||
# is discovered
|
||||
conf.update(local_config)
|
||||
col_kwargs = dict(collection_actions=COLLECTION_ACTIONS,
|
||||
member_actions=MEMBER_ACTIONS)
|
||||
|
||||
resources = {'network': 'networks',
|
||||
'subnet': 'subnets',
|
||||
'port': 'ports'}
|
||||
|
||||
def _map_resource(collection, resource, params):
|
||||
controller = base.create_resource(collection, resource,
|
||||
plugin, conf,
|
||||
params)
|
||||
mapper_kwargs = dict(controller=controller,
|
||||
requirements=REQUIREMENTS,
|
||||
**col_kwargs)
|
||||
return mapper.collection(collection, resource,
|
||||
**mapper_kwargs)
|
||||
|
||||
mapper.connect('index', '/', controller=Index(resources))
|
||||
for resource in resources:
|
||||
_map_resource(resources[resource], resource,
|
||||
RESOURCE_PARAM_MAP.get(resources[resource],
|
||||
dict()))
|
||||
|
||||
super(APIRouter, self).__init__(mapper)
|
40
quantum/api/v2/views.py
Normal file
40
quantum/api/v2/views.py
Normal file
@ -0,0 +1,40 @@
|
||||
# Copyright (c) 2012 OpenStack, LLC.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
def resource(data, keys):
|
||||
"""Formats the specified entity"""
|
||||
return dict(item for item in data.iteritems() if item[0] in keys)
|
||||
|
||||
|
||||
def port(port_data):
|
||||
"""Represents a view for a port object"""
|
||||
keys = ('id', 'network_id', 'mac_address', 'fixed_ips',
|
||||
'device_id', 'admin_state_up', 'tenant_id', 'op_status')
|
||||
return resource(port_data, keys)
|
||||
|
||||
|
||||
def network(network_data):
|
||||
"""Represents a view for a network object"""
|
||||
keys = ('id', 'name', 'subnets', 'admin_state_up', 'op_status',
|
||||
'tenant_id', 'mac_ranges')
|
||||
return resource(network_data, keys)
|
||||
|
||||
|
||||
def subnet(subnet_data):
|
||||
"""Represents a view for a subnet object"""
|
||||
keys = ('id', 'network_id', 'tenant_id', 'gateway_ip', 'ip_version',
|
||||
'prefix')
|
||||
return resource(subnet_data, keys)
|
@ -37,12 +37,16 @@ class Versions(object):
|
||||
version_objs = [
|
||||
{
|
||||
"id": "v1.0",
|
||||
"status": "CURRENT",
|
||||
"status": "DEPRECATED",
|
||||
},
|
||||
{
|
||||
"id": "v1.1",
|
||||
"status": "CURRENT",
|
||||
},
|
||||
{
|
||||
"id": "v2.0",
|
||||
"status": "PROPOSED",
|
||||
},
|
||||
]
|
||||
|
||||
if req.path != '/':
|
||||
|
@ -47,6 +47,14 @@ class NotFound(QuantumException):
|
||||
pass
|
||||
|
||||
|
||||
class NotAuthorized(QuantumException):
|
||||
message = _("Not authorized.")
|
||||
|
||||
|
||||
class AdminRequired(NotAuthorized):
|
||||
message = _("User does not have admin privileges: %(reason)s")
|
||||
|
||||
|
||||
class ClassNotFound(NotFound):
|
||||
message = _("Class %(class_name)s could not be found")
|
||||
|
||||
@ -55,6 +63,10 @@ class NetworkNotFound(NotFound):
|
||||
message = _("Network %(net_id)s could not be found")
|
||||
|
||||
|
||||
class SubnetNotFound(NotFound):
|
||||
message = _("Subnet %(subnet_id)s could not be found")
|
||||
|
||||
|
||||
class PortNotFound(NotFound):
|
||||
message = _("Port %(port_id)s could not be found "
|
||||
"on network %(net_id)s")
|
||||
@ -64,12 +76,16 @@ class StateInvalid(QuantumException):
|
||||
message = _("Unsupported port state: %(port_state)s")
|
||||
|
||||
|
||||
class NetworkInUse(QuantumException):
|
||||
class InUse(QuantumException):
|
||||
message = _("The resource is inuse")
|
||||
|
||||
|
||||
class NetworkInUse(InUse):
|
||||
message = _("Unable to complete operation on network %(net_id)s. "
|
||||
"There is one or more attachments plugged into its ports.")
|
||||
|
||||
|
||||
class PortInUse(QuantumException):
|
||||
class PortInUse(InUse):
|
||||
message = _("Unable to complete operation on port %(port_id)s "
|
||||
"for network %(net_id)s. The attachment '%(att_id)s"
|
||||
"is plugged into the logical port.")
|
||||
@ -112,3 +128,8 @@ class InvalidContentType(Invalid):
|
||||
|
||||
class NotImplementedError(Error):
|
||||
pass
|
||||
|
||||
|
||||
class FixedIPNotAvailable(QuantumException):
|
||||
message = _("Fixed IP (%(ip)s) unavailable for network "
|
||||
"%(network_uuid)s")
|
||||
|
@ -62,14 +62,29 @@ def bool_from_string(subject):
|
||||
|
||||
Useful for JSON-decoded stuff and config file parsing
|
||||
"""
|
||||
if type(subject) == type(bool):
|
||||
if isinstance(subject, bool):
|
||||
return subject
|
||||
if hasattr(subject, 'startswith'): # str or unicode...
|
||||
elif isinstance(subject, basestring):
|
||||
if subject.strip().lower() in ('true', 'on', '1'):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def boolize(subject):
|
||||
"""
|
||||
Quak like a boolean
|
||||
"""
|
||||
if isinstance(subject, bool):
|
||||
return subject
|
||||
elif isinstance(subject, basestring):
|
||||
sub = subject.strip().lower()
|
||||
if sub == 'true':
|
||||
return True
|
||||
elif sub == 'false':
|
||||
return False
|
||||
return subject
|
||||
|
||||
|
||||
def execute(cmd, process_input=None, addl_env=None, check_exit_code=True):
|
||||
logging.debug("Running cmd: %s", cmd)
|
||||
env = os.environ.copy()
|
||||
|
@ -27,7 +27,7 @@ from sqlalchemy.orm import sessionmaker, exc
|
||||
|
||||
from quantum.api.api_common import OperationalStatus
|
||||
from quantum.common import exceptions as q_exc
|
||||
from quantum.db import models
|
||||
from quantum.db import model_base, models
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@ -35,7 +35,7 @@ LOG = logging.getLogger(__name__)
|
||||
|
||||
_ENGINE = None
|
||||
_MAKER = None
|
||||
BASE = models.BASE
|
||||
BASE = model_base.BASE
|
||||
|
||||
|
||||
class MySQLPingListener(object):
|
||||
@ -79,15 +79,16 @@ def configure_db(options):
|
||||
engine_args['listeners'] = [MySQLPingListener()]
|
||||
|
||||
_ENGINE = create_engine(options['sql_connection'], **engine_args)
|
||||
if not register_models():
|
||||
base = options.get('base', BASE)
|
||||
if not register_models(base):
|
||||
if 'reconnect_interval' in options:
|
||||
retry_registration(options['reconnect_interval'])
|
||||
retry_registration(options['reconnect_interval'], base)
|
||||
|
||||
|
||||
def clear_db():
|
||||
def clear_db(base=BASE):
|
||||
global _ENGINE
|
||||
assert _ENGINE
|
||||
for table in reversed(BASE.metadata.sorted_tables):
|
||||
for table in reversed(base.metadata.sorted_tables):
|
||||
_ENGINE.execute(table.delete())
|
||||
|
||||
|
||||
@ -102,32 +103,32 @@ def get_session(autocommit=True, expire_on_commit=False):
|
||||
return _MAKER()
|
||||
|
||||
|
||||
def retry_registration(reconnect_interval):
|
||||
def retry_registration(reconnect_interval, base=BASE):
|
||||
while True:
|
||||
LOG.info("Unable to connect to database. Retrying in %s seconds" %
|
||||
reconnect_interval)
|
||||
time.sleep(reconnect_interval)
|
||||
if register_models():
|
||||
if register_models(base):
|
||||
break
|
||||
|
||||
|
||||
def register_models():
|
||||
def register_models(base=BASE):
|
||||
"""Register Models and create properties"""
|
||||
global _ENGINE
|
||||
assert _ENGINE
|
||||
try:
|
||||
BASE.metadata.create_all(_ENGINE)
|
||||
base.metadata.create_all(_ENGINE)
|
||||
except sql.exc.OperationalError as e:
|
||||
LOG.info("Database registration exception: %s" % e)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def unregister_models():
|
||||
def unregister_models(base=BASE):
|
||||
"""Unregister Models, useful clearing out data before testing"""
|
||||
global _ENGINE
|
||||
assert _ENGINE
|
||||
BASE.metadata.drop_all(_ENGINE)
|
||||
base.metadata.drop_all(_ENGINE)
|
||||
|
||||
|
||||
def network_create(tenant_id, name, op_status=OperationalStatus.UNKNOWN):
|
||||
@ -158,7 +159,7 @@ def network_get(net_id):
|
||||
return (session.query(models.Network).
|
||||
filter_by(uuid=net_id).
|
||||
one())
|
||||
except exc.NoResultFound, e:
|
||||
except exc.NoResultFound:
|
||||
raise q_exc.NetworkNotFound(net_id=net_id)
|
||||
|
||||
|
||||
@ -199,7 +200,7 @@ def validate_network_ownership(tenant_id, net_id):
|
||||
filter_by(uuid=net_id).
|
||||
filter_by(tenant_id=tenant_id).
|
||||
one())
|
||||
except exc.NoResultFound, e:
|
||||
except exc.NoResultFound:
|
||||
raise q_exc.NetworkNotFound(net_id=net_id)
|
||||
|
||||
|
||||
|
295
quantum/db/db_base_plugin_v2.py
Normal file
295
quantum/db/db_base_plugin_v2.py
Normal file
@ -0,0 +1,295 @@
|
||||
# Copyright (c) 2012 OpenStack, LLC.
|
||||
#
|
||||
# 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 logging
|
||||
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy.orm import exc
|
||||
|
||||
from quantum import quantum_plugin_base_v2
|
||||
from quantum.common import exceptions as q_exc
|
||||
from quantum.db import api as db
|
||||
from quantum.db import models_v2
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
|
||||
""" A class that implements the v2 Quantum plugin interface
|
||||
using SQLAlchemy models. Whenever a non-read call happens
|
||||
the plugin will call an event handler class method (e.g.,
|
||||
network_created()). The result is that this class can be
|
||||
sub-classed by other classes that add custom behaviors on
|
||||
certain events.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# NOTE(jkoelker) This is an incomlete implementation. Subclasses
|
||||
# must override __init__ and setup the database
|
||||
# and not call into this class's __init__.
|
||||
# This connection is setup as memory for the tests.
|
||||
sql_connection = 'sqlite:///:memory:'
|
||||
db.configure_db({'sql_connection': sql_connection,
|
||||
'base': models_v2.model_base.BASEV2})
|
||||
|
||||
def _get_tenant_id_for_create(self, context, resource):
|
||||
if context.is_admin and 'tenant_id' in resource:
|
||||
tenant_id = resource['tenant_id']
|
||||
elif ('tenant_id' in resource and
|
||||
resource['tenant_id'] != context.tenant_id):
|
||||
reason = _('Cannot create resource for another tenant')
|
||||
raise q_exc.AdminRequired(reason=reason)
|
||||
else:
|
||||
tenant_id = context.tenant_id
|
||||
return tenant_id
|
||||
|
||||
def _model_query(self, context, model):
|
||||
query = context.session.query(model)
|
||||
|
||||
# NOTE(jkoelker) non-admin queries are scoped to their tenant_id
|
||||
if not context.is_admin and hasattr(model.tenant_id):
|
||||
query = query.filter(tenant_id=context.tenant_id)
|
||||
|
||||
return query
|
||||
|
||||
def _get_by_id(self, context, model, id, joins=(), verbose=None):
|
||||
query = self._model_query(context, model)
|
||||
if verbose:
|
||||
if verbose and isinstance(verbose, list):
|
||||
options = [orm.joinedload(join) for join in joins
|
||||
if join in verbose]
|
||||
else:
|
||||
options = [orm.joinedload(join) for join in joins]
|
||||
query = query.options(*options)
|
||||
return query.filter_by(id=id).one()
|
||||
|
||||
def _get_network(self, context, id, verbose=None):
|
||||
try:
|
||||
network = self._get_by_id(context, models_v2.Network, id,
|
||||
joins=('subnets',), verbose=verbose)
|
||||
except exc.NoResultFound:
|
||||
raise q_exc.NetworkNotFound(net_id=id)
|
||||
except exc.MultipleResultsFound:
|
||||
LOG.error('Multiple networks match for %s' % id)
|
||||
raise q_exc.NetworkNotFound(net_id=id)
|
||||
return network
|
||||
|
||||
def _get_subnet(self, context, id, verbose=None):
|
||||
try:
|
||||
subnet = self._get_by_id(context, models_v2.Subnet, id,
|
||||
verbose=verbose)
|
||||
except exc.NoResultFound:
|
||||
raise q_exc.SubnetNotFound(subnet_id=id)
|
||||
except exc.MultipleResultsFound:
|
||||
LOG.error('Multiple subnets match for %s' % id)
|
||||
raise q_exc.SubnetNotFound(subnet_id=id)
|
||||
return subnet
|
||||
|
||||
def _get_port(self, context, id, verbose=None):
|
||||
try:
|
||||
port = self._get_by_id(context, models_v2.Port, id,
|
||||
verbose=verbose)
|
||||
except exc.NoResultFound:
|
||||
# NOTE(jkoelker) The PortNotFound exceptions requires net_id
|
||||
# kwarg in order to set the message correctly
|
||||
raise q_exc.PortNotFound(port_id=id, net_id=None)
|
||||
except exc.MultipleResultsFound:
|
||||
LOG.error('Multiple ports match for %s' % id)
|
||||
raise q_exc.PortNotFound(port_id=id)
|
||||
return port
|
||||
|
||||
def _fields(self, resource, fields):
|
||||
if fields:
|
||||
return dict(((key, item) for key, item in resource.iteritems()
|
||||
if key in fields))
|
||||
return resource
|
||||
|
||||
def _get_collection(self, context, model, dict_func, filters=None,
|
||||
fields=None, verbose=None):
|
||||
collection = self._model_query(context, model)
|
||||
if filters:
|
||||
for key, value in filters.iteritems():
|
||||
column = getattr(model, key, None)
|
||||
if column:
|
||||
collection = collection.filter(column.in_(value))
|
||||
return [dict_func(c, fields) for c in collection.all()]
|
||||
|
||||
def _make_network_dict(self, network, fields=None):
|
||||
res = {'id': network['id'],
|
||||
'name': network['name'],
|
||||
'tenant_id': network['tenant_id'],
|
||||
'admin_state_up': network['admin_state_up'],
|
||||
'op_status': network['op_status'],
|
||||
'subnets': [subnet['id']
|
||||
for subnet in network['subnets']]}
|
||||
|
||||
return self._fields(res, fields)
|
||||
|
||||
def _make_subnet_dict(self, subnet, fields=None):
|
||||
res = {'id': subnet['id'],
|
||||
'network_id': subnet['network_id'],
|
||||
'tenant_id': subnet['tenant_id'],
|
||||
'ip_version': subnet['ip_version'],
|
||||
'prefix': subnet['prefix'],
|
||||
'gateway_ip': subnet['gateway_ip']}
|
||||
return self._fields(res, fields)
|
||||
|
||||
def _make_port_dict(self, port, fields=None):
|
||||
res = {"id": port["id"],
|
||||
"network_id": port["network_id"],
|
||||
'tenant_id': port['tenant_id'],
|
||||
"mac_address": port["mac_address"],
|
||||
"admin_state_up": port["admin_state_up"],
|
||||
"op_status": port["op_status"],
|
||||
"fixed_ips": [ip["address"] for ip in port["fixed_ips"]],
|
||||
"device_id": port["device_id"]}
|
||||
return self._fields(res, fields)
|
||||
|
||||
def create_network(self, context, network):
|
||||
n = network['network']
|
||||
|
||||
# NOTE(jkoelker) Get the tenant_id outside of the session to avoid
|
||||
# unneeded db action if the operation raises
|
||||
tenant_id = self._get_tenant_id_for_create(context, n)
|
||||
with context.session.begin():
|
||||
network = models_v2.Network(tenant_id=tenant_id,
|
||||
name=n['name'],
|
||||
admin_state_up=n['admin_state_up'],
|
||||
op_status="ACTIVE")
|
||||
context.session.add(network)
|
||||
return self._make_network_dict(network)
|
||||
|
||||
def update_network(self, context, id, network):
|
||||
n = network['network']
|
||||
with context.session.begin():
|
||||
network = self._get_network(context, id)
|
||||
network.update(n)
|
||||
return self._make_network_dict(network)
|
||||
|
||||
def delete_network(self, context, id):
|
||||
with context.session.begin():
|
||||
network = self._get_network(context, id)
|
||||
|
||||
# TODO(anyone) Delegation?
|
||||
ports_qry = context.session.query(models_v2.Port)
|
||||
ports_qry.filter_by(network_id=id).delete()
|
||||
|
||||
subnets_qry = context.session.query(models_v2.Subnet)
|
||||
subnets_qry.filter_by(network_id=id).delete()
|
||||
|
||||
context.session.delete(network)
|
||||
|
||||
def get_network(self, context, id, fields=None, verbose=None):
|
||||
network = self._get_network(context, id, verbose=verbose)
|
||||
return self._make_network_dict(network, fields)
|
||||
|
||||
def get_networks(self, context, filters=None, fields=None, verbose=None):
|
||||
return self._get_collection(context, models_v2.Network,
|
||||
self._make_network_dict,
|
||||
filters=filters, fields=fields,
|
||||
verbose=verbose)
|
||||
|
||||
def create_subnet(self, context, subnet):
|
||||
s = subnet['subnet']
|
||||
# NOTE(jkoelker) Get the tenant_id outside of the session to avoid
|
||||
# unneeded db action if the operation raises
|
||||
tenant_id = self._get_tenant_id_for_create(context, s)
|
||||
with context.session.begin():
|
||||
subnet = models_v2.Subnet(tenant_id=tenant_id,
|
||||
network_id=s['network_id'],
|
||||
ip_version=s['ip_version'],
|
||||
prefix=s['prefix'],
|
||||
gateway_ip=s['gateway_ip'])
|
||||
|
||||
context.session.add(subnet)
|
||||
return self._make_subnet_dict(subnet)
|
||||
|
||||
def update_subnet(self, context, id, subnet):
|
||||
s = subnet['subnet']
|
||||
with context.session.begin():
|
||||
subnet = self._get_subnet(context, id)
|
||||
subnet.update(s)
|
||||
return self._make_subnet_dict(subnet)
|
||||
|
||||
def delete_subnet(self, context, id):
|
||||
with context.session.begin():
|
||||
subnet = self._get_subnet(context, id)
|
||||
|
||||
allocations_qry = context.session.query(models_v2.IPAllocation)
|
||||
allocations_qry.filter_by(subnet_id=id).delete()
|
||||
|
||||
context.session.delete(subnet)
|
||||
|
||||
def get_subnet(self, context, id, fields=None, verbose=None):
|
||||
subnet = self._get_subnet(context, id, verbose=verbose)
|
||||
return self._make_subnet_dict(subnet, fields)
|
||||
|
||||
def get_subnets(self, context, filters=None, fields=None, verbose=None):
|
||||
return self._get_collection(context, models_v2.Subnet,
|
||||
self._make_subnet_dict,
|
||||
filters=filters, fields=fields,
|
||||
verbose=verbose)
|
||||
|
||||
def create_port(self, context, port):
|
||||
p = port['port']
|
||||
# NOTE(jkoelker) Get the tenant_id outside of the session to avoid
|
||||
# unneeded db action if the operation raises
|
||||
tenant_id = self._get_tenant_id_for_create(context, p)
|
||||
|
||||
#FIXME(danwent): allocate MAC
|
||||
mac_address = p.get('mac_address', 'ca:fe:de:ad:be:ef')
|
||||
with context.session.begin():
|
||||
network = self._get_network(context, p["network_id"])
|
||||
|
||||
port = models_v2.Port(tenant_id=tenant_id,
|
||||
network_id=p['network_id'],
|
||||
mac_address=mac_address,
|
||||
admin_state_up=p['admin_state_up'],
|
||||
op_status="ACTIVE",
|
||||
device_id=p['device_id'])
|
||||
context.session.add(port)
|
||||
|
||||
# TODO(anyone) ip allocation
|
||||
#for subnet in network["subnets"]:
|
||||
# pass
|
||||
|
||||
return self._make_port_dict(port)
|
||||
|
||||
def update_port(self, context, id, port):
|
||||
p = port['port']
|
||||
with context.session.begin():
|
||||
port = self._get_port(context, id)
|
||||
port.update(p)
|
||||
return self._make_port_dict(port)
|
||||
|
||||
def delete_port(self, context, id):
|
||||
with context.session.begin():
|
||||
port = self._get_port(context, id)
|
||||
|
||||
allocations_qry = context.session.query(models_v2.IPAllocation)
|
||||
allocations_qry.filter_by(port_id=id).delete()
|
||||
|
||||
context.session.delete(port)
|
||||
|
||||
def get_port(self, context, id, fields=None, verbose=None):
|
||||
port = self._get_port(context, id, verbose=verbose)
|
||||
return self._make_port_dict(port, fields)
|
||||
|
||||
def get_ports(self, context, filters=None, fields=None, verbose=None):
|
||||
return self._get_collection(context, models_v2.Port,
|
||||
self._make_port_dict,
|
||||
filters=filters, fields=fields,
|
||||
verbose=verbose)
|
72
quantum/db/model_base.py
Normal file
72
quantum/db/model_base.py
Normal file
@ -0,0 +1,72 @@
|
||||
# Copyright (c) 2012 OpenStack, LLC.
|
||||
#
|
||||
# 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 uuid
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy.ext import declarative
|
||||
|
||||
|
||||
def str_uuid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
class QuantumBase(object):
|
||||
"""Base class for Quantum Models."""
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
setattr(self, key, value)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return getattr(self, key)
|
||||
|
||||
def get(self, key, default=None):
|
||||
return getattr(self, key, default)
|
||||
|
||||
def __iter__(self):
|
||||
self._i = iter(orm.object_mapper(self).columns)
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
n = self._i.next().name
|
||||
return n, getattr(self, n)
|
||||
|
||||
def update(self, values):
|
||||
"""Make the model object behave like a dict"""
|
||||
for k, v in values.iteritems():
|
||||
setattr(self, k, v)
|
||||
|
||||
def iteritems(self):
|
||||
"""Make the model object behave like a dict.
|
||||
Includes attributes from joins."""
|
||||
local = dict(self)
|
||||
joined = dict([(k, v) for k, v in self.__dict__.iteritems()
|
||||
if not k[0] == '_'])
|
||||
local.update(joined)
|
||||
return local.iteritems()
|
||||
|
||||
|
||||
class QuantumBaseV2(QuantumBase):
|
||||
id = sa.Column(sa.String(36), primary_key=True, default=str_uuid)
|
||||
|
||||
@declarative.declared_attr
|
||||
def __tablename__(cls):
|
||||
# NOTE(jkoelker) use the pluralized name of the class as the table
|
||||
return cls.__name__.lower() + 's'
|
||||
|
||||
|
||||
BASE = declarative.declarative_base(cls=QuantumBase)
|
||||
BASEV2 = declarative.declarative_base(cls=QuantumBaseV2)
|
@ -21,51 +21,16 @@
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import Column, String, ForeignKey
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import relation, object_mapper
|
||||
from sqlalchemy.orm import relation
|
||||
|
||||
from quantum.api import api_common as common
|
||||
from quantum.db import model_base
|
||||
|
||||
|
||||
BASE = declarative_base()
|
||||
BASE = model_base.BASE
|
||||
|
||||
|
||||
class QuantumBase(object):
|
||||
"""Base class for Quantum Models."""
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
setattr(self, key, value)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return getattr(self, key)
|
||||
|
||||
def get(self, key, default=None):
|
||||
return getattr(self, key, default)
|
||||
|
||||
def __iter__(self):
|
||||
self._i = iter(object_mapper(self).columns)
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
n = self._i.next().name
|
||||
return n, getattr(self, n)
|
||||
|
||||
def update(self, values):
|
||||
"""Make the model object behave like a dict"""
|
||||
for k, v in values.iteritems():
|
||||
setattr(self, k, v)
|
||||
|
||||
def iteritems(self):
|
||||
"""Make the model object behave like a dict.
|
||||
Includes attributes from joins."""
|
||||
local = dict(self)
|
||||
joined = dict([(k, v) for k, v in self.__dict__.iteritems()
|
||||
if not k[0] == '_'])
|
||||
local.update(joined)
|
||||
return local.iteritems()
|
||||
|
||||
|
||||
class Port(BASE, QuantumBase):
|
||||
class Port(model_base.BASE):
|
||||
"""Represents a port on a quantum network"""
|
||||
__tablename__ = 'ports'
|
||||
|
||||
@ -90,7 +55,7 @@ class Port(BASE, QuantumBase):
|
||||
self.interface_id)
|
||||
|
||||
|
||||
class Network(BASE, QuantumBase):
|
||||
class Network(model_base.BASE):
|
||||
"""Represents a quantum network"""
|
||||
__tablename__ = 'networks'
|
||||
|
||||
|
72
quantum/db/models_v2.py
Normal file
72
quantum/db/models_v2.py
Normal file
@ -0,0 +1,72 @@
|
||||
# Copyright (c) 2012 OpenStack, LLC.
|
||||
#
|
||||
# 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 sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
|
||||
from quantum.db import model_base
|
||||
|
||||
|
||||
class HasTenant(object):
|
||||
"""Tenant mixin, add to subclasses that have a tenant."""
|
||||
# NOTE(jkoelker) tenant_id is just a free form string ;(
|
||||
tenant_id = sa.Column(sa.String(255))
|
||||
|
||||
|
||||
class IPAllocation(model_base.BASEV2):
|
||||
"""Internal representation of a IP address allocation in a Quantum
|
||||
subnet
|
||||
"""
|
||||
port_id = sa.Column(sa.String(36), sa.ForeignKey('ports.id'))
|
||||
address = sa.Column(sa.String(16), nullable=False, primary_key=True)
|
||||
subnet_id = sa.Column(sa.String(36), sa.ForeignKey('subnets.id'),
|
||||
primary_key=True)
|
||||
allocated = sa.Column(sa.Boolean(), nullable=False)
|
||||
|
||||
|
||||
class Port(model_base.BASEV2, HasTenant):
|
||||
"""Represents a port on a quantum v2 network"""
|
||||
network_id = sa.Column(sa.String(36), sa.ForeignKey("networks.id"),
|
||||
nullable=False)
|
||||
fixed_ips = orm.relationship(IPAllocation, backref='ports')
|
||||
mac_address = sa.Column(sa.String(32), nullable=False)
|
||||
admin_state_up = sa.Column(sa.Boolean(), nullable=False)
|
||||
op_status = sa.Column(sa.String(16), nullable=False)
|
||||
device_id = sa.Column(sa.String(255), nullable=False)
|
||||
|
||||
|
||||
class Subnet(model_base.BASEV2, HasTenant):
|
||||
"""Represents a quantum subnet"""
|
||||
network_id = sa.Column(sa.String(36), sa.ForeignKey('networks.id'))
|
||||
allocations = orm.relationship(IPAllocation,
|
||||
backref=orm.backref('subnet',
|
||||
uselist=False))
|
||||
ip_version = sa.Column(sa.Integer, nullable=False)
|
||||
prefix = sa.Column(sa.String(255), nullable=False)
|
||||
gateway_ip = sa.Column(sa.String(255))
|
||||
|
||||
#TODO(danwent):
|
||||
# - dns_namservers
|
||||
# - excluded_ranges
|
||||
# - additional_routes
|
||||
|
||||
|
||||
class Network(model_base.BASEV2, HasTenant):
|
||||
"""Represents a v2 quantum network"""
|
||||
name = sa.Column(sa.String(255))
|
||||
ports = orm.relationship(Port, backref='networks')
|
||||
subnets = orm.relationship(Subnet, backref='networks')
|
||||
op_status = sa.Column(sa.String(16))
|
||||
admin_state_up = sa.Column(sa.Boolean)
|
@ -30,7 +30,6 @@ from quantum.common import utils
|
||||
from quantum.common.config import find_config_file
|
||||
from quantum.common.exceptions import ClassNotFound
|
||||
from quantum.openstack.common import importutils
|
||||
from quantum.quantum_plugin_base import QuantumPluginBase
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@ -46,6 +45,29 @@ def find_config(basepath):
|
||||
return None
|
||||
|
||||
|
||||
def get_plugin(plugin_provider):
|
||||
# If the plugin can't be found let them know gracefully
|
||||
try:
|
||||
LOG.info("Loading Plugin: %s" % plugin_provider)
|
||||
plugin_klass = importutils.import_class(plugin_provider)
|
||||
except ClassNotFound:
|
||||
LOG.exception("Error loading plugin")
|
||||
raise Exception("Plugin not found. You can install a "
|
||||
"plugin with: pip install <plugin-name>\n"
|
||||
"Example: pip install quantum-sample-plugin")
|
||||
return plugin_klass()
|
||||
|
||||
|
||||
def get_plugin_provider(options, config_file=None):
|
||||
if config_file:
|
||||
config_file = [config_file]
|
||||
|
||||
if not 'plugin_provider' in options:
|
||||
cf = find_config_file(options, config_file, CONFIG_FILE)
|
||||
options['plugin_provider'] = utils.get_plugin_from_config(cf)
|
||||
return options['plugin_provider']
|
||||
|
||||
|
||||
class QuantumManager(object):
|
||||
|
||||
_instance = None
|
||||
@ -55,31 +77,13 @@ class QuantumManager(object):
|
||||
if not options:
|
||||
options = {}
|
||||
|
||||
if config_file:
|
||||
config_file = [config_file]
|
||||
|
||||
self.configuration_file = find_config_file(options, config_file,
|
||||
CONFIG_FILE)
|
||||
if not 'plugin_provider' in options:
|
||||
options['plugin_provider'] = utils.get_plugin_from_config(
|
||||
self.configuration_file)
|
||||
LOG.debug("Plugin location:%s", options['plugin_provider'])
|
||||
|
||||
# If the plugin can't be found let them know gracefully
|
||||
try:
|
||||
plugin_klass = importutils.import_class(options['plugin_provider'])
|
||||
except ClassNotFound:
|
||||
raise Exception("Plugin not found. You can install a "
|
||||
"plugin with: pip install <plugin-name>\n"
|
||||
"Example: pip install quantum-sample-plugin")
|
||||
|
||||
if not issubclass(plugin_klass, QuantumPluginBase):
|
||||
raise Exception("Configured Quantum plug-in "
|
||||
"didn't pass compatibility test")
|
||||
else:
|
||||
LOG.debug("Successfully imported Quantum plug-in."
|
||||
"All compatibility tests passed")
|
||||
self.plugin = plugin_klass()
|
||||
# NOTE(jkoelker) Testing for the subclass with the __subclasshook__
|
||||
# breaks tach monitoring. It has been removed
|
||||
# intentianally to allow v2 plugins to be monitored
|
||||
# for performance metrics.
|
||||
plugin_provider = get_plugin_provider(options, config_file)
|
||||
LOG.debug("Plugin location:%s", plugin_provider)
|
||||
self.plugin = get_plugin(plugin_provider)
|
||||
|
||||
@classmethod
|
||||
def get_plugin(cls, options=None, config_file=None):
|
||||
|
121
quantum/plugins/sample/SamplePluginV2.py
Normal file
121
quantum/plugins/sample/SamplePluginV2.py
Normal file
@ -0,0 +1,121 @@
|
||||
# Copyright (c) 2012 OpenStack, LLC.
|
||||
#
|
||||
# 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 logging
|
||||
import uuid
|
||||
|
||||
from quantum import quantum_plugin_base_v2
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class QuantumEchoPlugin(quantum_plugin_base_v2.QuantumPluginBaseV2):
|
||||
|
||||
"""
|
||||
QuantumEchoPlugin is a demo plugin that doesn't
|
||||
do anything but demonstrate the concept of a
|
||||
concrete Quantum Plugin. Any call to this plugin
|
||||
will result in just a log statement with the name
|
||||
method that was called and its arguments.
|
||||
"""
|
||||
|
||||
def _log(self, name, context, **kwargs):
|
||||
kwarg_msg = ' '.join([('%s: |%s|' % (str(key), kwargs[key]))
|
||||
for key in kwargs])
|
||||
|
||||
# TODO(anyone) Add a nice __repr__ and __str__ to context
|
||||
#LOG.debug('%s context: %s %s' % (name, context, kwarg_msg))
|
||||
LOG.debug('%s %s' % (name, kwarg_msg))
|
||||
|
||||
def create_subnet(self, context, subnet):
|
||||
self._log("create_subnet", context, subnet=subnet)
|
||||
res = {"id": str(uuid.uuid4())}
|
||||
res.update(subnet)
|
||||
return res
|
||||
|
||||
def update_subnet(self, context, id, subnet):
|
||||
self._log("update_subnet", context, id=id, subnet=subnet)
|
||||
res = {"id": id}
|
||||
res.update(subnet)
|
||||
return res
|
||||
|
||||
def get_subnet(self, context, id, show=None, verbose=None):
|
||||
self._log("get_subnet", context, id=id, show=show,
|
||||
verbose=verbose)
|
||||
return {"id": id}
|
||||
|
||||
def delete_subnet(self, context, id):
|
||||
self._log("delete_subnet", context, id=id)
|
||||
|
||||
def get_subnets(self, context, filters=None, show=None, verbose=None):
|
||||
self._log("get_subnets", context, filters=filters, show=show,
|
||||
verbose=verbose)
|
||||
return []
|
||||
|
||||
def create_network(self, context, network):
|
||||
self._log("create_network", context, network=network)
|
||||
res = {"id": str(uuid.uuid4())}
|
||||
res.update(network)
|
||||
return res
|
||||
|
||||
def update_network(self, context, id, network):
|
||||
self._log("update_network", context, id=id, network=network)
|
||||
res = {"id": id}
|
||||
res.update(network)
|
||||
return res
|
||||
|
||||
def get_network(self, context, id, show=None, verbose=None):
|
||||
self._log("get_network", context, id=id, show=show,
|
||||
verbose=verbose)
|
||||
return {"id": id}
|
||||
|
||||
def delete_network(self, context, id):
|
||||
self._log("delete_network", context, id=id)
|
||||
|
||||
def get_networks(self, context, filters=None, show=None, verbose=None):
|
||||
self._log("get_networks", context, filters=filters, show=show,
|
||||
verbose=verbose)
|
||||
return []
|
||||
|
||||
def create_port(self, context, port):
|
||||
self._log("create_port", context, port=port)
|
||||
res = {"id": str(uuid.uuid4())}
|
||||
res.update(port)
|
||||
return res
|
||||
|
||||
def update_port(self, context, id, port):
|
||||
self._log("update_port", context, id=id, port=port)
|
||||
res = {"id": id}
|
||||
res.update(port)
|
||||
return res
|
||||
|
||||
def get_port(self, context, id, show=None, verbose=None):
|
||||
self._log("get_port", context, id=id, show=show,
|
||||
verbose=verbose)
|
||||
return {"id": id}
|
||||
|
||||
def delete_port(self, context, id):
|
||||
self._log("delete_port", context, id=id)
|
||||
|
||||
def get_ports(self, context, filters=None, show=None, verbose=None):
|
||||
self._log("get_ports", context, filters=filters, show=show,
|
||||
verbose=verbose)
|
||||
return []
|
||||
|
||||
supported_extension_aliases = ["FOXNSOX"]
|
||||
|
||||
def method_to_support_foxnsox_extension(self, context):
|
||||
self._log("method_to_support_foxnsox_extension", context)
|
195
quantum/quantum_plugin_base_v2.py
Normal file
195
quantum/quantum_plugin_base_v2.py
Normal file
@ -0,0 +1,195 @@
|
||||
# Copyright 2011 Nicira Networks, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# @author: Dan Wendlandt, Nicira, Inc.
|
||||
|
||||
"""
|
||||
v2 Quantum Plug-in API specification.
|
||||
|
||||
QuantumPluginBase provides the definition of minimum set of
|
||||
methods that needs to be implemented by a v2 Quantum Plug-in.
|
||||
"""
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
|
||||
class QuantumPluginBaseV2(object):
|
||||
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
@abstractmethod
|
||||
def create_subnet(self, context, subnet):
|
||||
"""
|
||||
Create a subnet, which represents a range of IP addresses
|
||||
that can be allocated to devices
|
||||
: param subnet_data: data describing the prefix
|
||||
{
|
||||
"network_id": UUID of the network to which this subnet
|
||||
is bound.
|
||||
"ip_version": integer indicating IP protocol version.
|
||||
example: 4
|
||||
"prefix": string indicating IP prefix indicating addresses
|
||||
that can be allocated for devices on this subnet.
|
||||
example: "10.0.0.0/24"
|
||||
"gateway_ip": string indicating the default gateway
|
||||
for devices on this subnet. example: "10.0.0.1"
|
||||
"dns_nameservers": list of strings stricting indication the
|
||||
DNS name servers for devices on this
|
||||
subnet. example: [ "8.8.8.8", "8.8.4.4" ]
|
||||
"excluded_ranges" : list of dicts indicating pairs of IPs that
|
||||
should not be allocated from the prefix.
|
||||
example: [ { "start" : "10.0.0.2",
|
||||
"end" : "10.0.0.5" } ]
|
||||
"additional_routes": list of dicts indicating routes beyond
|
||||
the default gateway and local prefix route
|
||||
that should be injected into the device.
|
||||
example: [{"destination": "192.168.0.0/16",
|
||||
"nexthop": "10.0.0.5" } ]
|
||||
}
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update_subnet(self, context, id, subnet):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_subnet(self, context, id, fields=None, verbose=None):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete_subnet(self, context, id):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_subnets(self, context, filters=None, fields=None, verbose=None):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def create_network(self, context, network):
|
||||
"""
|
||||
Creates a new Virtual Network, assigns a name and associates
|
||||
|
||||
:param net_data:
|
||||
{
|
||||
'name': a human-readable name associated
|
||||
with network referenced by net-id
|
||||
example: "net-1"
|
||||
'admin-state-up': indicates whether this network should
|
||||
be operational.
|
||||
'subnets': list of subnet uuids associated with this
|
||||
network.
|
||||
}
|
||||
:raises:
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update_network(self, context, id, network):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete_network(self, context, id):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_network(self, context, id, fields=None, verbose=None):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_networks(self, context, filters=None, fields=None, verbose=None):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def create_port(self, context, port):
|
||||
"""
|
||||
Creates a port on the specified Virtual Network. Optionally
|
||||
specify customization of port IP-related attributes, otherwise
|
||||
the port gets the default values of these attributes associated with
|
||||
the subnet.
|
||||
|
||||
:param port_data:
|
||||
{"network_id" : UUID of network that this port is attached to.
|
||||
"admin-state-up" : boolean indicating whether this port should be
|
||||
operational.
|
||||
"mac_address" : (optional) mac address used on this port. If no
|
||||
value is specified, the plugin will generate a
|
||||
MAC address based on internal configuration.
|
||||
"fixed_ips" : (optional) list of dictionaries describing the
|
||||
fixed IPs to be allocated for use by the device on
|
||||
this port. If not specified, the plugin will
|
||||
attempt to find a v4 and v6 subnet associated
|
||||
with the network and allocate an IP for that
|
||||
subnet.
|
||||
Note: "address" is optional, in which case an
|
||||
address from the specified subnet is
|
||||
selected.
|
||||
example: [{"subnet": "<uuid>",
|
||||
"address": "10.0.0.9"}]
|
||||
"routes" : (optional) list of routes to be injected into this
|
||||
device. If not specified, the port will get a
|
||||
route for its local subnet, a route for the default
|
||||
gateway, and each of the routes in the
|
||||
'additional_routes' field of the subnet.
|
||||
example: [ { "destination" : "192.168.0.0/16",
|
||||
"nexthop" : "10.0.0.5" } ]
|
||||
}
|
||||
:raises: exception.NetworkNotFound
|
||||
:raises: exception.RequestedFixedIPNotAvailable
|
||||
:raises: exception.FixedIPNotAvailable
|
||||
:raises: exception.RouteInvalid
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update_port(self, context, id, port):
|
||||
"""
|
||||
Updates the attributes of a specific port on the
|
||||
specified Virtual Network.
|
||||
|
||||
:returns: a mapping sequence with the following signature:
|
||||
{'port-id': uuid representing the
|
||||
updated port on specified quantum network
|
||||
'port-state': update port state( UP or DOWN)
|
||||
}
|
||||
:raises: exception.StateInvalid
|
||||
:raises: exception.PortNotFound
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete_port(self, context, id):
|
||||
"""
|
||||
Deletes a port on a specified Virtual Network,
|
||||
if the port contains a remote interface attachment,
|
||||
the remote interface is first un-plugged and then the port
|
||||
is deleted.
|
||||
|
||||
:returns: a mapping sequence with the following signature:
|
||||
{'port-id': uuid representing the deleted port
|
||||
on specified quantum network
|
||||
}
|
||||
:raises: exception.PortInUse
|
||||
:raises: exception.PortNotFound
|
||||
:raises: exception.NetworkNotFound
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_port(self, context, id, fields=None, verbose=None):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_ports(self, context, filters=None, fields=None, verbose=None):
|
||||
pass
|
486
quantum/tests/unit/test_api_v2.py
Normal file
486
quantum/tests/unit/test_api_v2.py
Normal file
@ -0,0 +1,486 @@
|
||||
# Copyright 2012 OpenStack LLC.
|
||||
# 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 spec
|
||||
|
||||
import logging
|
||||
import unittest
|
||||
import uuid
|
||||
|
||||
import mock
|
||||
import webtest
|
||||
|
||||
from webob import exc
|
||||
|
||||
from quantum.common import exceptions as q_exc
|
||||
from quantum.api.v2 import resource as wsgi_resource
|
||||
from quantum.api.v2 import router
|
||||
from quantum.api.v2 import views
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_path(resource, id=None, fmt=None):
|
||||
path = '/%s' % resource
|
||||
|
||||
if id is not None:
|
||||
path = path + '/%s' % id
|
||||
|
||||
if fmt is not None:
|
||||
path = path + '.%s' % fmt
|
||||
|
||||
return path
|
||||
|
||||
|
||||
class V2WsgiResourceTestCase(unittest.TestCase):
|
||||
def test_unmapped_quantum_error(self):
|
||||
controller = mock.MagicMock()
|
||||
controller.test.side_effect = q_exc.QuantumException()
|
||||
|
||||
resource = webtest.TestApp(wsgi_resource.Resource(controller))
|
||||
|
||||
environ = {'wsgiorg.routing_args': (None, {'action': 'test'})}
|
||||
res = resource.get('', extra_environ=environ, expect_errors=True)
|
||||
self.assertEqual(res.status_int, exc.HTTPInternalServerError.code)
|
||||
|
||||
def test_mapped_quantum_error(self):
|
||||
controller = mock.MagicMock()
|
||||
controller.test.side_effect = q_exc.QuantumException()
|
||||
|
||||
faults = {q_exc.QuantumException: exc.HTTPGatewayTimeout}
|
||||
resource = webtest.TestApp(wsgi_resource.Resource(controller,
|
||||
faults=faults))
|
||||
|
||||
environ = {'wsgiorg.routing_args': (None, {'action': 'test'})}
|
||||
res = resource.get('', extra_environ=environ, expect_errors=True)
|
||||
self.assertEqual(res.status_int, exc.HTTPGatewayTimeout.code)
|
||||
|
||||
def test_http_error(self):
|
||||
controller = mock.MagicMock()
|
||||
controller.test.side_effect = exc.HTTPGatewayTimeout()
|
||||
|
||||
resource = webtest.TestApp(wsgi_resource.Resource(controller))
|
||||
|
||||
environ = {'wsgiorg.routing_args': (None, {'action': 'test'})}
|
||||
res = resource.get('', extra_environ=environ, expect_errors=True)
|
||||
self.assertEqual(res.status_int, exc.HTTPGatewayTimeout.code)
|
||||
|
||||
def test_unhandled_error(self):
|
||||
controller = mock.MagicMock()
|
||||
controller.test.side_effect = Exception()
|
||||
|
||||
resource = webtest.TestApp(wsgi_resource.Resource(controller))
|
||||
|
||||
environ = {'wsgiorg.routing_args': (None, {'action': 'test'})}
|
||||
res = resource.get('', extra_environ=environ, expect_errors=True)
|
||||
self.assertEqual(res.status_int, exc.HTTPInternalServerError.code)
|
||||
|
||||
|
||||
class ResourceIndexTestCase(unittest.TestCase):
|
||||
def test_index_json(self):
|
||||
index = webtest.TestApp(router.Index({'foo': 'bar'}))
|
||||
res = index.get('')
|
||||
|
||||
self.assertTrue('resources' in res.json)
|
||||
self.assertTrue(len(res.json['resources']) == 1)
|
||||
|
||||
resource = res.json['resources'][0]
|
||||
self.assertTrue('collection' in resource)
|
||||
self.assertTrue(resource['collection'] == 'bar')
|
||||
|
||||
self.assertTrue('name' in resource)
|
||||
self.assertTrue(resource['name'] == 'foo')
|
||||
|
||||
self.assertTrue('links' in resource)
|
||||
self.assertTrue(len(resource['links']) == 1)
|
||||
|
||||
link = resource['links'][0]
|
||||
self.assertTrue('href' in link)
|
||||
self.assertTrue(link['href'] == 'http://localhost/bar')
|
||||
self.assertTrue('rel' in link)
|
||||
self.assertTrue(link['rel'] == 'self')
|
||||
|
||||
|
||||
class APIv2TestCase(unittest.TestCase):
|
||||
# NOTE(jkoelker) This potentially leaks the mock object if the setUp
|
||||
# raises without being caught. Using unittest2
|
||||
# or dropping 2.6 support so we can use addCleanup
|
||||
# will get around this.
|
||||
def setUp(self):
|
||||
plugin = 'quantum.quantum_plugin_base_v2.QuantumPluginBaseV2'
|
||||
self._plugin_patcher = mock.patch(plugin, autospec=True)
|
||||
self.plugin = self._plugin_patcher.start()
|
||||
|
||||
api = router.APIRouter({'plugin_provider': plugin})
|
||||
self.api = webtest.TestApp(api)
|
||||
|
||||
def tearDown(self):
|
||||
self._plugin_patcher.stop()
|
||||
self.api = None
|
||||
self.plugin = None
|
||||
|
||||
def test_verbose_attr(self):
|
||||
instance = self.plugin.return_value
|
||||
instance.get_networks.return_value = []
|
||||
|
||||
self.api.get(_get_path('networks'), {'verbose': 'foo'})
|
||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||
filters=mock.ANY,
|
||||
fields=mock.ANY,
|
||||
verbose=['foo'])
|
||||
|
||||
def test_multiple_verbose_attr(self):
|
||||
instance = self.plugin.return_value
|
||||
instance.get_networks.return_value = []
|
||||
|
||||
self.api.get(_get_path('networks'), {'verbose': ['foo', 'bar']})
|
||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||
filters=mock.ANY,
|
||||
fields=mock.ANY,
|
||||
verbose=['foo',
|
||||
'bar'])
|
||||
|
||||
def test_verbose_false_str(self):
|
||||
instance = self.plugin.return_value
|
||||
instance.get_networks.return_value = []
|
||||
|
||||
self.api.get(_get_path('networks'), {'verbose': 'false'})
|
||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||
filters=mock.ANY,
|
||||
fields=mock.ANY,
|
||||
verbose=False)
|
||||
|
||||
def test_verbose_true_str(self):
|
||||
instance = self.plugin.return_value
|
||||
instance.get_networks.return_value = []
|
||||
|
||||
self.api.get(_get_path('networks'), {'verbose': 'true'})
|
||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||
filters=mock.ANY,
|
||||
fields=mock.ANY,
|
||||
verbose=True)
|
||||
|
||||
def test_verbose_true_trump_attr(self):
|
||||
instance = self.plugin.return_value
|
||||
instance.get_networks.return_value = []
|
||||
|
||||
self.api.get(_get_path('networks'), {'verbose': ['true', 'foo']})
|
||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||
filters=mock.ANY,
|
||||
fields=mock.ANY,
|
||||
verbose=True)
|
||||
|
||||
def test_verbose_false_trump_attr(self):
|
||||
instance = self.plugin.return_value
|
||||
instance.get_networks.return_value = []
|
||||
|
||||
self.api.get(_get_path('networks'), {'verbose': ['false', 'foo']})
|
||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||
filters=mock.ANY,
|
||||
fields=mock.ANY,
|
||||
verbose=False)
|
||||
|
||||
def test_verbose_true_trump_false(self):
|
||||
instance = self.plugin.return_value
|
||||
instance.get_networks.return_value = []
|
||||
|
||||
self.api.get(_get_path('networks'), {'verbose': ['true', 'false']})
|
||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||
filters=mock.ANY,
|
||||
fields=mock.ANY,
|
||||
verbose=True)
|
||||
|
||||
def test_fields(self):
|
||||
instance = self.plugin.return_value
|
||||
instance.get_networks.return_value = []
|
||||
|
||||
self.api.get(_get_path('networks'), {'fields': 'foo'})
|
||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||
filters=mock.ANY,
|
||||
fields=['foo'],
|
||||
verbose=mock.ANY)
|
||||
|
||||
def test_fields_multiple(self):
|
||||
instance = self.plugin.return_value
|
||||
instance.get_networks.return_value = []
|
||||
|
||||
self.api.get(_get_path('networks'), {'fields': ['foo', 'bar']})
|
||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||
filters=mock.ANY,
|
||||
fields=['foo', 'bar'],
|
||||
verbose=mock.ANY)
|
||||
|
||||
def test_fields_multiple_with_empty(self):
|
||||
instance = self.plugin.return_value
|
||||
instance.get_networks.return_value = []
|
||||
|
||||
self.api.get(_get_path('networks'), {'fields': ['foo', '']})
|
||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||
filters=mock.ANY,
|
||||
fields=['foo'],
|
||||
verbose=mock.ANY)
|
||||
|
||||
def test_fields_empty(self):
|
||||
instance = self.plugin.return_value
|
||||
instance.get_networks.return_value = []
|
||||
|
||||
self.api.get(_get_path('networks'), {'fields': ''})
|
||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||
filters=mock.ANY,
|
||||
fields=[],
|
||||
verbose=mock.ANY)
|
||||
|
||||
def test_fields_multiple_empty(self):
|
||||
instance = self.plugin.return_value
|
||||
instance.get_networks.return_value = []
|
||||
|
||||
self.api.get(_get_path('networks'), {'fields': ['', '']})
|
||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||
filters=mock.ANY,
|
||||
fields=[],
|
||||
verbose=mock.ANY)
|
||||
|
||||
def test_filters(self):
|
||||
instance = self.plugin.return_value
|
||||
instance.get_networks.return_value = []
|
||||
|
||||
self.api.get(_get_path('networks'), {'foo': 'bar'})
|
||||
filters = {'foo': ['bar']}
|
||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||
filters=filters,
|
||||
fields=mock.ANY,
|
||||
verbose=mock.ANY)
|
||||
|
||||
def test_filters_empty(self):
|
||||
instance = self.plugin.return_value
|
||||
instance.get_networks.return_value = []
|
||||
|
||||
self.api.get(_get_path('networks'), {'foo': ''})
|
||||
filters = {}
|
||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||
filters=filters,
|
||||
fields=mock.ANY,
|
||||
verbose=mock.ANY)
|
||||
|
||||
def test_filters_multiple_empty(self):
|
||||
instance = self.plugin.return_value
|
||||
instance.get_networks.return_value = []
|
||||
|
||||
self.api.get(_get_path('networks'), {'foo': ['', '']})
|
||||
filters = {}
|
||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||
filters=filters,
|
||||
fields=mock.ANY,
|
||||
verbose=mock.ANY)
|
||||
|
||||
def test_filters_multiple_with_empty(self):
|
||||
instance = self.plugin.return_value
|
||||
instance.get_networks.return_value = []
|
||||
|
||||
self.api.get(_get_path('networks'), {'foo': ['bar', '']})
|
||||
filters = {'foo': ['bar']}
|
||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||
filters=filters,
|
||||
fields=mock.ANY,
|
||||
verbose=mock.ANY)
|
||||
|
||||
def test_filters_multiple_values(self):
|
||||
instance = self.plugin.return_value
|
||||
instance.get_networks.return_value = []
|
||||
|
||||
self.api.get(_get_path('networks'), {'foo': ['bar', 'bar2']})
|
||||
filters = {'foo': ['bar', 'bar2']}
|
||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||
filters=filters,
|
||||
fields=mock.ANY,
|
||||
verbose=mock.ANY)
|
||||
|
||||
def test_filters_multiple(self):
|
||||
instance = self.plugin.return_value
|
||||
instance.get_networks.return_value = []
|
||||
|
||||
self.api.get(_get_path('networks'), {'foo': 'bar',
|
||||
'foo2': 'bar2'})
|
||||
filters = {'foo': ['bar'], 'foo2': ['bar2']}
|
||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||
filters=filters,
|
||||
fields=mock.ANY,
|
||||
verbose=mock.ANY)
|
||||
|
||||
def test_filters_with_fields(self):
|
||||
instance = self.plugin.return_value
|
||||
instance.get_networks.return_value = []
|
||||
|
||||
self.api.get(_get_path('networks'), {'foo': 'bar', 'fields': 'foo'})
|
||||
filters = {'foo': ['bar']}
|
||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||
filters=filters,
|
||||
fields=['foo'],
|
||||
verbose=mock.ANY)
|
||||
|
||||
def test_filters_with_verbose(self):
|
||||
instance = self.plugin.return_value
|
||||
instance.get_networks.return_value = []
|
||||
|
||||
self.api.get(_get_path('networks'), {'foo': 'bar',
|
||||
'verbose': 'true'})
|
||||
filters = {'foo': ['bar']}
|
||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||
filters=filters,
|
||||
fields=mock.ANY,
|
||||
verbose=True)
|
||||
|
||||
def test_filters_with_fields_and_verbose(self):
|
||||
instance = self.plugin.return_value
|
||||
instance.get_networks.return_value = []
|
||||
|
||||
self.api.get(_get_path('networks'), {'foo': 'bar',
|
||||
'fields': 'foo',
|
||||
'verbose': 'true'})
|
||||
filters = {'foo': ['bar']}
|
||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||
filters=filters,
|
||||
fields=['foo'],
|
||||
verbose=True)
|
||||
|
||||
|
||||
class JSONV2TestCase(APIv2TestCase):
|
||||
def test_list(self):
|
||||
return_value = [{'network': {'name': 'net1',
|
||||
'admin_state_up': True,
|
||||
'subnets': []}}]
|
||||
instance = self.plugin.return_value
|
||||
instance.get_networks.return_value = return_value
|
||||
|
||||
res = self.api.get(_get_path('networks'))
|
||||
self.assertTrue('networks' in res.json)
|
||||
|
||||
def test_create(self):
|
||||
data = {'network': {'name': 'net1', 'admin_state_up': True}}
|
||||
return_value = {'subnets': []}
|
||||
return_value.update(data['network'].copy())
|
||||
|
||||
instance = self.plugin.return_value
|
||||
instance.create_network.return_value = return_value
|
||||
|
||||
res = self.api.post_json(_get_path('networks'), data)
|
||||
self.assertEqual(res.status_int, exc.HTTPCreated.code)
|
||||
|
||||
def test_create_no_body(self):
|
||||
data = {'whoa': None}
|
||||
res = self.api.post_json(_get_path('networks'), data,
|
||||
expect_errors=True)
|
||||
self.assertEqual(res.status_int, exc.HTTPBadRequest.code)
|
||||
|
||||
def test_create_no_resource(self):
|
||||
res = self.api.post_json(_get_path('networks'), dict(),
|
||||
expect_errors=True)
|
||||
self.assertEqual(res.status_int, exc.HTTPBadRequest.code)
|
||||
|
||||
def test_create_missing_attr(self):
|
||||
data = {'network': {'what': 'who'}}
|
||||
res = self.api.post_json(_get_path('networks'), data,
|
||||
expect_errors=True)
|
||||
self.assertEqual(res.status_int, 422)
|
||||
|
||||
def test_create_bulk(self):
|
||||
data = {'networks': [{'name': 'net1', 'admin_state_up': True},
|
||||
{'name': 'net2', 'admin_state_up': True}]}
|
||||
|
||||
def side_effect(context, network):
|
||||
nets = network.copy()
|
||||
for net in nets['networks']:
|
||||
net.update({'subnets': []})
|
||||
return nets
|
||||
|
||||
instance = self.plugin.return_value
|
||||
instance.create_network.side_effect = side_effect
|
||||
|
||||
res = self.api.post_json(_get_path('networks'), data)
|
||||
self.assertEqual(res.status_int, exc.HTTPCreated.code)
|
||||
|
||||
def test_create_bulk_no_networks(self):
|
||||
data = {'networks': []}
|
||||
res = self.api.post_json(_get_path('networks'), data,
|
||||
expect_errors=True)
|
||||
self.assertEqual(res.status_int, exc.HTTPBadRequest.code)
|
||||
|
||||
def test_create_bulk_missing_attr(self):
|
||||
data = {'networks': [{'what': 'who'}]}
|
||||
res = self.api.post_json(_get_path('networks'), data,
|
||||
expect_errors=True)
|
||||
self.assertEqual(res.status_int, 422)
|
||||
|
||||
def test_create_bulk_partial_body(self):
|
||||
data = {'networks': [{'name': 'net1', 'admin_state_up': True},
|
||||
{}]}
|
||||
res = self.api.post_json(_get_path('networks'), data,
|
||||
expect_errors=True)
|
||||
self.assertEqual(res.status_int, 422)
|
||||
|
||||
def test_fields(self):
|
||||
return_value = {'name': 'net1', 'admin_state_up': True,
|
||||
'subnets': []}
|
||||
|
||||
instance = self.plugin.return_value
|
||||
instance.get_network.return_value = return_value
|
||||
|
||||
self.api.get(_get_path('networks', id=str(uuid.uuid4())))
|
||||
|
||||
def test_delete(self):
|
||||
instance = self.plugin.return_value
|
||||
instance.delete_network.return_value = None
|
||||
|
||||
res = self.api.delete(_get_path('networks', id=str(uuid.uuid4())))
|
||||
self.assertEqual(res.status_int, exc.HTTPNoContent.code)
|
||||
|
||||
def test_update(self):
|
||||
data = {'network': {'name': 'net1', 'admin_state_up': True}}
|
||||
return_value = {'subnets': []}
|
||||
return_value.update(data['network'].copy())
|
||||
|
||||
instance = self.plugin.return_value
|
||||
instance.update_network.return_value = return_value
|
||||
|
||||
self.api.put_json(_get_path('networks',
|
||||
id=str(uuid.uuid4())), data)
|
||||
|
||||
|
||||
class V2Views(unittest.TestCase):
|
||||
def _view(self, keys, func):
|
||||
data = dict((key, 'value') for key in keys)
|
||||
data['fake'] = 'value'
|
||||
res = func(data)
|
||||
self.assertTrue('fake' not in res)
|
||||
for key in keys:
|
||||
self.assertTrue(key in res)
|
||||
|
||||
def test_resource(self):
|
||||
res = views.resource({'one': 1, 'two': 2}, ['one'])
|
||||
self.assertTrue('one' in res)
|
||||
self.assertTrue('two' not in res)
|
||||
|
||||
def test_network(self):
|
||||
keys = ('id', 'name', 'subnets', 'admin_state_up', 'op_status',
|
||||
'tenant_id', 'mac_ranges')
|
||||
self._view(keys, views.network)
|
||||
|
||||
def test_port(self):
|
||||
keys = ('id', 'network_id', 'mac_address', 'fixed_ips',
|
||||
'device_id', 'admin_state_up', 'tenant_id', 'op_status')
|
||||
self._view(keys, views.port)
|
||||
|
||||
def test_subnet(self):
|
||||
keys = ('id', 'network_id', 'tenant_id', 'gateway_ip',
|
||||
'ip_version', 'prefix')
|
||||
self._view(keys, views.subnet)
|
317
quantum/tests/unit/test_db_plugin.py
Normal file
317
quantum/tests/unit/test_db_plugin.py
Normal file
@ -0,0 +1,317 @@
|
||||
# Copyright (c) 2012 OpenStack, LLC.
|
||||
#
|
||||
# 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 logging
|
||||
import unittest
|
||||
import contextlib
|
||||
|
||||
from quantum.api.v2.router import APIRouter
|
||||
from quantum.db import api as db
|
||||
from quantum.tests.unit.testlib_api import create_request
|
||||
from quantum.wsgi import Serializer, JSONDeserializer
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class QuantumDbPluginV2TestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
super(QuantumDbPluginV2TestCase, self).setUp()
|
||||
|
||||
# NOTE(jkoelker) for a 'pluggable' framework, Quantum sure
|
||||
# doesn't like when the plugin changes ;)
|
||||
db._ENGINE = None
|
||||
db._MAKER = None
|
||||
|
||||
self._tenant_id = 'test-tenant'
|
||||
|
||||
json_deserializer = JSONDeserializer()
|
||||
self._deserializers = {
|
||||
'application/json': json_deserializer,
|
||||
}
|
||||
|
||||
plugin = 'quantum.db.db_base_plugin_v2.QuantumDbPluginV2'
|
||||
self.api = APIRouter({'plugin_provider': plugin})
|
||||
|
||||
def tearDown(self):
|
||||
super(QuantumDbPluginV2TestCase, self).tearDown()
|
||||
# NOTE(jkoelker) for a 'pluggable' framework, Quantum sure
|
||||
# doesn't like when the plugin changes ;)
|
||||
db._ENGINE = None
|
||||
db._MAKER = None
|
||||
|
||||
def _req(self, method, resource, data=None, fmt='json', id=None):
|
||||
if id:
|
||||
path = '/%(resource)s/%(id)s.%(fmt)s' % locals()
|
||||
else:
|
||||
path = '/%(resource)s.%(fmt)s' % locals()
|
||||
content_type = 'application/%s' % fmt
|
||||
body = None
|
||||
if data:
|
||||
body = Serializer().serialize(data, content_type)
|
||||
return create_request(path, body, content_type, method)
|
||||
|
||||
def new_create_request(self, resource, data, fmt='json'):
|
||||
return self._req('POST', resource, data, fmt)
|
||||
|
||||
def new_list_request(self, resource, fmt='json'):
|
||||
return self._req('GET', resource, None, fmt)
|
||||
|
||||
def new_show_request(self, resource, id, fmt='json'):
|
||||
return self._req('GET', resource, None, fmt, id=id)
|
||||
|
||||
def new_delete_request(self, resource, id, fmt='json'):
|
||||
return self._req('DELETE', resource, None, fmt, id=id)
|
||||
|
||||
def new_update_request(self, resource, data, id, fmt='json'):
|
||||
return self._req('PUT', resource, data, fmt, id=id)
|
||||
|
||||
def deserialize(self, content_type, response):
|
||||
ctype = 'application/%s' % content_type
|
||||
data = self._deserializers[ctype].\
|
||||
deserialize(response.body)['body']
|
||||
return data
|
||||
|
||||
def _create_network(self, fmt, name, admin_status_up):
|
||||
data = {'network': {'name': name,
|
||||
'admin_state_up': admin_status_up}}
|
||||
network_req = self.new_create_request('networks', data, fmt)
|
||||
return network_req.get_response(self.api)
|
||||
|
||||
def _create_subnet(self, fmt, net_id, gateway_ip, prefix):
|
||||
data = {'subnet': {'network_id': net_id,
|
||||
'allocations': [],
|
||||
'prefix': prefix,
|
||||
'ip_version': 4,
|
||||
'gateway_ip': gateway_ip}}
|
||||
subnet_req = self.new_create_request('subnets', data, fmt)
|
||||
return subnet_req.get_response(self.api)
|
||||
|
||||
def _make_subnet(self, fmt, network, gateway, prefix):
|
||||
res = self._create_subnet(fmt, network['network']['id'],
|
||||
gateway, prefix)
|
||||
return self.deserialize(fmt, res)
|
||||
|
||||
def _delete(self, collection, id):
|
||||
req = self.new_delete_request(collection, id)
|
||||
req.get_response(self.api)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def network(self, name='net1', admin_status_up=True, fmt='json'):
|
||||
res = self._create_network(fmt, name, admin_status_up)
|
||||
network = self.deserialize(fmt, res)
|
||||
yield network
|
||||
self._delete('networks', network['network']['id'])
|
||||
|
||||
@contextlib.contextmanager
|
||||
def subnet(self, network=None, gateway='10.0.0.1',
|
||||
prefix='10.0.0.0/24', fmt='json'):
|
||||
# TODO(anyone) DRY this
|
||||
if not network:
|
||||
with self.network() as network:
|
||||
subnet = self._make_subnet(fmt, network, gateway, prefix)
|
||||
yield subnet
|
||||
self._delete('subnets', subnet['subnet']['id'])
|
||||
else:
|
||||
subnet = self._make_subnet(fmt, network, gateway, prefix)
|
||||
yield subnet
|
||||
self._delete('subnets', subnet['subnet']['id'])
|
||||
|
||||
|
||||
class TestV2HTTPResponse(QuantumDbPluginV2TestCase):
|
||||
def test_create_returns_201(self):
|
||||
res = self._create_network('json', 'net2', True)
|
||||
self.assertEquals(res.status_int, 201)
|
||||
|
||||
def test_list_returns_200(self):
|
||||
req = self.new_list_request('networks')
|
||||
res = req.get_response(self.api)
|
||||
self.assertEquals(res.status_int, 200)
|
||||
|
||||
def test_show_returns_200(self):
|
||||
with self.network() as net:
|
||||
req = self.new_show_request('networks', net['network']['id'])
|
||||
res = req.get_response(self.api)
|
||||
self.assertEquals(res.status_int, 200)
|
||||
|
||||
def test_delete_returns_204(self):
|
||||
res = self._create_network('json', 'net1', True)
|
||||
net = self.deserialize('json', res)
|
||||
req = self.new_delete_request('networks', net['network']['id'])
|
||||
res = req.get_response(self.api)
|
||||
self.assertEquals(res.status_int, 204)
|
||||
|
||||
def test_update_returns_200(self):
|
||||
with self.network() as net:
|
||||
req = self.new_update_request('networks',
|
||||
{'network': {'name': 'steve'}},
|
||||
net['network']['id'])
|
||||
res = req.get_response(self.api)
|
||||
self.assertEquals(res.status_int, 200)
|
||||
|
||||
def test_bad_route_404(self):
|
||||
req = self.new_list_request('doohickeys')
|
||||
res = req.get_response(self.api)
|
||||
self.assertEquals(res.status_int, 404)
|
||||
|
||||
|
||||
#class TestPortsV2(APIv2TestCase):
|
||||
# def setUp(self):
|
||||
# super(TestPortsV2, self).setUp()
|
||||
# res = self._create_network('json', 'net1', True)
|
||||
# data = self._deserializers['application/json'].\
|
||||
# deserialize(res.body)['body']
|
||||
# self.net_id = data['network']['id']
|
||||
#
|
||||
# def _create_port(self, fmt, net_id, admin_state_up, device_id,
|
||||
# custom_req_body=None,
|
||||
# expected_res_status=None):
|
||||
# content_type = 'application/' + fmt
|
||||
# data = {'port': {'network_id': net_id,
|
||||
# 'admin_state_up': admin_state_up,
|
||||
# 'device_id': device_id}}
|
||||
# port_req = self.new_create_request('ports', data, fmt)
|
||||
# port_res = port_req.get_response(self.api)
|
||||
# return json.loads(port_res.body)
|
||||
#
|
||||
# def test_create_port_json(self):
|
||||
# port = self._create_port('json', self.net_id, True, 'dev_id_1')
|
||||
# self.assertEqual(port['id'], 'dev_id_1')
|
||||
# self.assertEqual(port['admin_state_up'], 'DOWN')
|
||||
# self.assertEqual(port['device_id'], 'dev_id_1')
|
||||
# self.assertTrue('mac_address' in port)
|
||||
# self.assertTrue('op_status' in port)
|
||||
#
|
||||
# def test_list_ports(self):
|
||||
# port1 = self._create_port('json', self.net_id, True, 'dev_id_1')
|
||||
# port2 = self._create_port('json', self.net_id, True, 'dev_id_2')
|
||||
#
|
||||
# res = self.new_list_request('ports', 'json')
|
||||
# port_list = json.loads(res.body)['body']
|
||||
# self.assertTrue(port1 in port_list['ports'])
|
||||
# self.assertTrue(port2 in port_list['ports'])
|
||||
#
|
||||
# def test_show_port(self):
|
||||
# port = self._create_port('json', self.net_id, True, 'dev_id_1')
|
||||
# res = self.new_show_request('port', 'json', port['id'])
|
||||
# port = json.loads(res.body)['body']
|
||||
# self.assertEquals(port['port']['name'], 'dev_id_1')
|
||||
#
|
||||
# def test_delete_port(self):
|
||||
# port = self._create_port('json', self.net_id, True, 'dev_id_1')
|
||||
# self.new_delete_request('port', 'json', port['id'])
|
||||
#
|
||||
# port = self.new_show_request('port', 'json', port['id'])
|
||||
#
|
||||
# self.assertEquals(res.status_int, 404)
|
||||
#
|
||||
# def test_update_port(self):
|
||||
# port = self._create_port('json', self.net_id, True, 'dev_id_1')
|
||||
# port_body = {'port': {'device_id': 'Bob'}}
|
||||
# res = self.new_update_request('port', port_body, port['id'])
|
||||
# port = json.loads(res.body)['body']
|
||||
# self.assertEquals(port['device_id'], 'Bob')
|
||||
#
|
||||
# def test_delete_non_existent_port_404(self):
|
||||
# res = self.new_delete_request('port', 'json', 1)
|
||||
# self.assertEquals(res.status_int, 404)
|
||||
#
|
||||
# def test_show_non_existent_port_404(self):
|
||||
# res = self.new_show_request('port', 'json', 1)
|
||||
# self.assertEquals(res.status_int, 404)
|
||||
#
|
||||
# def test_update_non_existent_port_404(self):
|
||||
# res = self.new_update_request('port', 'json', 1)
|
||||
# self.assertEquals(res.status_int, 404)
|
||||
|
||||
|
||||
class TestNetworksV2(QuantumDbPluginV2TestCase):
|
||||
# NOTE(cerberus): successful network update and delete are
|
||||
# effectively tested above
|
||||
def test_create_network(self):
|
||||
name = 'net1'
|
||||
keys = [('subnets', []), ('name', name), ('admin_state_up', True),
|
||||
('op_status', 'ACTIVE')]
|
||||
with self.network(name=name) as net:
|
||||
for k, v in keys:
|
||||
self.assertEquals(net['network'][k], v)
|
||||
|
||||
def test_list_networks(self):
|
||||
with self.network(name='net1') as net1:
|
||||
with self.network(name='net2') as net2:
|
||||
req = self.new_list_request('networks')
|
||||
res = self.deserialize('json', req.get_response(self.api))
|
||||
|
||||
self.assertEquals(res['networks'][0]['name'],
|
||||
net1['network']['name'])
|
||||
self.assertEquals(res['networks'][1]['name'],
|
||||
net2['network']['name'])
|
||||
|
||||
def test_show_network(self):
|
||||
with self.network(name='net1') as net:
|
||||
req = self.new_show_request('networks', net['network']['id'])
|
||||
res = self.deserialize('json', req.get_response(self.api))
|
||||
self.assertEquals(res['network']['name'],
|
||||
net['network']['name'])
|
||||
|
||||
|
||||
class TestSubnetsV2(QuantumDbPluginV2TestCase):
|
||||
def test_create_subnet(self):
|
||||
gateway = '10.0.0.1'
|
||||
prefix = '10.0.0.0/24'
|
||||
keys = [('ip_version', 4), ('gateway_ip', gateway),
|
||||
('prefix', prefix)]
|
||||
with self.subnet(gateway=gateway, prefix=prefix) as subnet:
|
||||
for k, v in keys:
|
||||
self.assertEquals(subnet['subnet'][k], v)
|
||||
|
||||
def test_update_subnet(self):
|
||||
with self.subnet() as subnet:
|
||||
data = {'subnet': {'network_id': 'blarg',
|
||||
'prefix': '192.168.0.0/24'}}
|
||||
req = self.new_update_request('subnets', data,
|
||||
subnet['subnet']['id'])
|
||||
res = self.deserialize('json', req.get_response(self.api))
|
||||
self.assertEqual(res['subnet']['prefix'],
|
||||
data['subnet']['prefix'])
|
||||
|
||||
def test_show_subnet(self):
|
||||
with self.network() as network:
|
||||
with self.subnet(network=network) as subnet:
|
||||
req = self.new_show_request('subnets',
|
||||
subnet['subnet']['id'])
|
||||
res = self.deserialize('json', req.get_response(self.api))
|
||||
self.assertEquals(res['subnet']['id'],
|
||||
subnet['subnet']['id'])
|
||||
self.assertEquals(res['subnet']['network_id'],
|
||||
network['network']['id'])
|
||||
|
||||
def test_list_subnets(self):
|
||||
# NOTE(jkoelker) This would be a good place to use contextlib.nested
|
||||
# or just drop 2.6 support ;)
|
||||
with self.network() as network:
|
||||
with self.subnet(network=network, gateway='10.0.0.1',
|
||||
prefix='10.0.1.0/24') as subnet:
|
||||
with self.subnet(network=network, gateway='10.0.1.1',
|
||||
prefix='10.0.1.0/24') as subnet2:
|
||||
req = self.new_list_request('subnets')
|
||||
res = self.deserialize('json',
|
||||
req.get_response(self.api))
|
||||
res1 = res['subnets'][0]
|
||||
res2 = res['subnets'][1]
|
||||
self.assertEquals(res1['prefix'],
|
||||
subnet['subnet']['prefix'])
|
||||
self.assertEquals(res2['prefix'],
|
||||
subnet2['subnet']['prefix'])
|
@ -89,6 +89,33 @@ class Middleware(object):
|
||||
behavior.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def factory(cls, global_config, **local_config):
|
||||
"""Used for paste app factories in paste.deploy config files.
|
||||
|
||||
Any local configuration (that is, values under the [filter:APPNAME]
|
||||
section of the paste config) will be passed into the `__init__` method
|
||||
as kwargs.
|
||||
|
||||
A hypothetical configuration would look like:
|
||||
|
||||
[filter:analytics]
|
||||
redis_host = 127.0.0.1
|
||||
paste.filter_factory = nova.api.analytics:Analytics.factory
|
||||
|
||||
which would result in a call to the `Analytics` class as
|
||||
|
||||
import nova.api.analytics
|
||||
analytics.Analytics(app_from_paste, redis_host='127.0.0.1')
|
||||
|
||||
You could of course re-implement the `factory` method in subclasses,
|
||||
but using the kwarg passing it shouldn't be necessary.
|
||||
|
||||
"""
|
||||
def _factory(app):
|
||||
return cls(app, **local_config)
|
||||
return _factory
|
||||
|
||||
def __init__(self, application):
|
||||
self.application = application
|
||||
|
||||
@ -204,6 +231,12 @@ class XMLDictSerializer(DictSerializer):
|
||||
|
||||
return self.to_xml_string(node)
|
||||
|
||||
def __call__(self, data):
|
||||
# Provides a migration path to a cleaner WSGI layer, this
|
||||
# "default" stuff and extreme extensibility isn't being used
|
||||
# like originally intended
|
||||
return self.default(data)
|
||||
|
||||
def to_xml_string(self, node, has_atom=False):
|
||||
self._add_xmlns(node, has_atom)
|
||||
return node.toxml('UTF-8')
|
||||
@ -427,6 +460,10 @@ class XMLDeserializer(TextDeserializer):
|
||||
def default(self, datastring):
|
||||
return {'body': self._from_xml(datastring)}
|
||||
|
||||
def __call__(self, datastring):
|
||||
# Adding a migration path to allow us to remove unncessary classes
|
||||
return self.default(datastring)
|
||||
|
||||
|
||||
class RequestHeadersDeserializer(ActionDispatcher):
|
||||
"""Default request headers deserializer"""
|
||||
|
@ -4,5 +4,5 @@ Routes>=1.12.3
|
||||
eventlet>=0.9.12
|
||||
lxml
|
||||
python-gflags==1.3
|
||||
sqlalchemy
|
||||
sqlalchemy>0.6.4
|
||||
webob==1.2.0
|
||||
|
@ -1,6 +1,6 @@
|
||||
distribute>=0.6.24
|
||||
coverage
|
||||
mock>=0.7.1
|
||||
mock>=0.8
|
||||
mox==0.5.3
|
||||
nose
|
||||
nosexcover
|
||||
|
Loading…
Reference in New Issue
Block a user