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
|
/: quantumversions
|
||||||
/v1.0: quantumapi_v1_0
|
/v1.0: quantumapi_v1_0
|
||||||
/v1.1: quantumapi_v1_1
|
/v1.1: quantumapi_v1_1
|
||||||
|
/v2.0: quantumapi_v2_0
|
||||||
|
|
||||||
[pipeline:quantumapi_v1_0]
|
[pipeline:quantumapi_v1_0]
|
||||||
# By default, authentication is disabled.
|
# By default, authentication is disabled.
|
||||||
@ -38,6 +39,13 @@ pipeline = extensions quantumapiapp_v1_0
|
|||||||
pipeline = extensions quantumapiapp_v1_1
|
pipeline = extensions quantumapiapp_v1_1
|
||||||
# pipeline = authtoken keystonecontext 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]
|
[filter:keystonecontext]
|
||||||
paste.filter_factory = quantum.auth:QuantumKeystoneContext.factory
|
paste.filter_factory = quantum.auth:QuantumKeystoneContext.factory
|
||||||
|
|
||||||
@ -61,3 +69,6 @@ paste.app_factory = quantum.api:APIRouterV10.factory
|
|||||||
|
|
||||||
[app:quantumapiapp_v1_1]
|
[app:quantumapiapp_v1_1]
|
||||||
paste.app_factory = quantum.api:APIRouterV11.factory
|
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 = [
|
version_objs = [
|
||||||
{
|
{
|
||||||
"id": "v1.0",
|
"id": "v1.0",
|
||||||
"status": "CURRENT",
|
"status": "DEPRECATED",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "v1.1",
|
"id": "v1.1",
|
||||||
"status": "CURRENT",
|
"status": "CURRENT",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "v2.0",
|
||||||
|
"status": "PROPOSED",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
if req.path != '/':
|
if req.path != '/':
|
||||||
|
@ -47,6 +47,14 @@ class NotFound(QuantumException):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotAuthorized(QuantumException):
|
||||||
|
message = _("Not authorized.")
|
||||||
|
|
||||||
|
|
||||||
|
class AdminRequired(NotAuthorized):
|
||||||
|
message = _("User does not have admin privileges: %(reason)s")
|
||||||
|
|
||||||
|
|
||||||
class ClassNotFound(NotFound):
|
class ClassNotFound(NotFound):
|
||||||
message = _("Class %(class_name)s could not be found")
|
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")
|
message = _("Network %(net_id)s could not be found")
|
||||||
|
|
||||||
|
|
||||||
|
class SubnetNotFound(NotFound):
|
||||||
|
message = _("Subnet %(subnet_id)s could not be found")
|
||||||
|
|
||||||
|
|
||||||
class PortNotFound(NotFound):
|
class PortNotFound(NotFound):
|
||||||
message = _("Port %(port_id)s could not be found "
|
message = _("Port %(port_id)s could not be found "
|
||||||
"on network %(net_id)s")
|
"on network %(net_id)s")
|
||||||
@ -64,12 +76,16 @@ class StateInvalid(QuantumException):
|
|||||||
message = _("Unsupported port state: %(port_state)s")
|
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. "
|
message = _("Unable to complete operation on network %(net_id)s. "
|
||||||
"There is one or more attachments plugged into its ports.")
|
"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 "
|
message = _("Unable to complete operation on port %(port_id)s "
|
||||||
"for network %(net_id)s. The attachment '%(att_id)s"
|
"for network %(net_id)s. The attachment '%(att_id)s"
|
||||||
"is plugged into the logical port.")
|
"is plugged into the logical port.")
|
||||||
@ -112,3 +128,8 @@ class InvalidContentType(Invalid):
|
|||||||
|
|
||||||
class NotImplementedError(Error):
|
class NotImplementedError(Error):
|
||||||
pass
|
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
|
Useful for JSON-decoded stuff and config file parsing
|
||||||
"""
|
"""
|
||||||
if type(subject) == type(bool):
|
if isinstance(subject, bool):
|
||||||
return subject
|
return subject
|
||||||
if hasattr(subject, 'startswith'): # str or unicode...
|
elif isinstance(subject, basestring):
|
||||||
if subject.strip().lower() in ('true', 'on', '1'):
|
if subject.strip().lower() in ('true', 'on', '1'):
|
||||||
return True
|
return True
|
||||||
return False
|
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):
|
def execute(cmd, process_input=None, addl_env=None, check_exit_code=True):
|
||||||
logging.debug("Running cmd: %s", cmd)
|
logging.debug("Running cmd: %s", cmd)
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
|
@ -27,7 +27,7 @@ from sqlalchemy.orm import sessionmaker, exc
|
|||||||
|
|
||||||
from quantum.api.api_common import OperationalStatus
|
from quantum.api.api_common import OperationalStatus
|
||||||
from quantum.common import exceptions as q_exc
|
from quantum.common import exceptions as q_exc
|
||||||
from quantum.db import models
|
from quantum.db import model_base, models
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
@ -35,7 +35,7 @@ LOG = logging.getLogger(__name__)
|
|||||||
|
|
||||||
_ENGINE = None
|
_ENGINE = None
|
||||||
_MAKER = None
|
_MAKER = None
|
||||||
BASE = models.BASE
|
BASE = model_base.BASE
|
||||||
|
|
||||||
|
|
||||||
class MySQLPingListener(object):
|
class MySQLPingListener(object):
|
||||||
@ -79,15 +79,16 @@ def configure_db(options):
|
|||||||
engine_args['listeners'] = [MySQLPingListener()]
|
engine_args['listeners'] = [MySQLPingListener()]
|
||||||
|
|
||||||
_ENGINE = create_engine(options['sql_connection'], **engine_args)
|
_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:
|
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
|
global _ENGINE
|
||||||
assert _ENGINE
|
assert _ENGINE
|
||||||
for table in reversed(BASE.metadata.sorted_tables):
|
for table in reversed(base.metadata.sorted_tables):
|
||||||
_ENGINE.execute(table.delete())
|
_ENGINE.execute(table.delete())
|
||||||
|
|
||||||
|
|
||||||
@ -102,32 +103,32 @@ def get_session(autocommit=True, expire_on_commit=False):
|
|||||||
return _MAKER()
|
return _MAKER()
|
||||||
|
|
||||||
|
|
||||||
def retry_registration(reconnect_interval):
|
def retry_registration(reconnect_interval, base=BASE):
|
||||||
while True:
|
while True:
|
||||||
LOG.info("Unable to connect to database. Retrying in %s seconds" %
|
LOG.info("Unable to connect to database. Retrying in %s seconds" %
|
||||||
reconnect_interval)
|
reconnect_interval)
|
||||||
time.sleep(reconnect_interval)
|
time.sleep(reconnect_interval)
|
||||||
if register_models():
|
if register_models(base):
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def register_models():
|
def register_models(base=BASE):
|
||||||
"""Register Models and create properties"""
|
"""Register Models and create properties"""
|
||||||
global _ENGINE
|
global _ENGINE
|
||||||
assert _ENGINE
|
assert _ENGINE
|
||||||
try:
|
try:
|
||||||
BASE.metadata.create_all(_ENGINE)
|
base.metadata.create_all(_ENGINE)
|
||||||
except sql.exc.OperationalError as e:
|
except sql.exc.OperationalError as e:
|
||||||
LOG.info("Database registration exception: %s" % e)
|
LOG.info("Database registration exception: %s" % e)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def unregister_models():
|
def unregister_models(base=BASE):
|
||||||
"""Unregister Models, useful clearing out data before testing"""
|
"""Unregister Models, useful clearing out data before testing"""
|
||||||
global _ENGINE
|
global _ENGINE
|
||||||
assert _ENGINE
|
assert _ENGINE
|
||||||
BASE.metadata.drop_all(_ENGINE)
|
base.metadata.drop_all(_ENGINE)
|
||||||
|
|
||||||
|
|
||||||
def network_create(tenant_id, name, op_status=OperationalStatus.UNKNOWN):
|
def network_create(tenant_id, name, op_status=OperationalStatus.UNKNOWN):
|
||||||
@ -158,7 +159,7 @@ def network_get(net_id):
|
|||||||
return (session.query(models.Network).
|
return (session.query(models.Network).
|
||||||
filter_by(uuid=net_id).
|
filter_by(uuid=net_id).
|
||||||
one())
|
one())
|
||||||
except exc.NoResultFound, e:
|
except exc.NoResultFound:
|
||||||
raise q_exc.NetworkNotFound(net_id=net_id)
|
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(uuid=net_id).
|
||||||
filter_by(tenant_id=tenant_id).
|
filter_by(tenant_id=tenant_id).
|
||||||
one())
|
one())
|
||||||
except exc.NoResultFound, e:
|
except exc.NoResultFound:
|
||||||
raise q_exc.NetworkNotFound(net_id=net_id)
|
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
|
import uuid
|
||||||
|
|
||||||
from sqlalchemy import Column, String, ForeignKey
|
from sqlalchemy import Column, String, ForeignKey
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.orm import relation
|
||||||
from sqlalchemy.orm import relation, object_mapper
|
|
||||||
|
|
||||||
from quantum.api import api_common as common
|
from quantum.api import api_common as common
|
||||||
|
from quantum.db import model_base
|
||||||
|
|
||||||
|
|
||||||
BASE = declarative_base()
|
BASE = model_base.BASE
|
||||||
|
|
||||||
|
|
||||||
class QuantumBase(object):
|
class Port(model_base.BASE):
|
||||||
"""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):
|
|
||||||
"""Represents a port on a quantum network"""
|
"""Represents a port on a quantum network"""
|
||||||
__tablename__ = 'ports'
|
__tablename__ = 'ports'
|
||||||
|
|
||||||
@ -90,7 +55,7 @@ class Port(BASE, QuantumBase):
|
|||||||
self.interface_id)
|
self.interface_id)
|
||||||
|
|
||||||
|
|
||||||
class Network(BASE, QuantumBase):
|
class Network(model_base.BASE):
|
||||||
"""Represents a quantum network"""
|
"""Represents a quantum network"""
|
||||||
__tablename__ = 'networks'
|
__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.config import find_config_file
|
||||||
from quantum.common.exceptions import ClassNotFound
|
from quantum.common.exceptions import ClassNotFound
|
||||||
from quantum.openstack.common import importutils
|
from quantum.openstack.common import importutils
|
||||||
from quantum.quantum_plugin_base import QuantumPluginBase
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
@ -46,6 +45,29 @@ def find_config(basepath):
|
|||||||
return None
|
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):
|
class QuantumManager(object):
|
||||||
|
|
||||||
_instance = None
|
_instance = None
|
||||||
@ -55,31 +77,13 @@ class QuantumManager(object):
|
|||||||
if not options:
|
if not options:
|
||||||
options = {}
|
options = {}
|
||||||
|
|
||||||
if config_file:
|
# NOTE(jkoelker) Testing for the subclass with the __subclasshook__
|
||||||
config_file = [config_file]
|
# breaks tach monitoring. It has been removed
|
||||||
|
# intentianally to allow v2 plugins to be monitored
|
||||||
self.configuration_file = find_config_file(options, config_file,
|
# for performance metrics.
|
||||||
CONFIG_FILE)
|
plugin_provider = get_plugin_provider(options, config_file)
|
||||||
if not 'plugin_provider' in options:
|
LOG.debug("Plugin location:%s", plugin_provider)
|
||||||
options['plugin_provider'] = utils.get_plugin_from_config(
|
self.plugin = get_plugin(plugin_provider)
|
||||||
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()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_plugin(cls, options=None, config_file=None):
|
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.
|
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):
|
def __init__(self, application):
|
||||||
self.application = application
|
self.application = application
|
||||||
|
|
||||||
@ -204,6 +231,12 @@ class XMLDictSerializer(DictSerializer):
|
|||||||
|
|
||||||
return self.to_xml_string(node)
|
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):
|
def to_xml_string(self, node, has_atom=False):
|
||||||
self._add_xmlns(node, has_atom)
|
self._add_xmlns(node, has_atom)
|
||||||
return node.toxml('UTF-8')
|
return node.toxml('UTF-8')
|
||||||
@ -427,6 +460,10 @@ class XMLDeserializer(TextDeserializer):
|
|||||||
def default(self, datastring):
|
def default(self, datastring):
|
||||||
return {'body': self._from_xml(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):
|
class RequestHeadersDeserializer(ActionDispatcher):
|
||||||
"""Default request headers deserializer"""
|
"""Default request headers deserializer"""
|
||||||
|
@ -4,5 +4,5 @@ Routes>=1.12.3
|
|||||||
eventlet>=0.9.12
|
eventlet>=0.9.12
|
||||||
lxml
|
lxml
|
||||||
python-gflags==1.3
|
python-gflags==1.3
|
||||||
sqlalchemy
|
sqlalchemy>0.6.4
|
||||||
webob==1.2.0
|
webob==1.2.0
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
distribute>=0.6.24
|
distribute>=0.6.24
|
||||||
coverage
|
coverage
|
||||||
mock>=0.7.1
|
mock>=0.8
|
||||||
mox==0.5.3
|
mox==0.5.3
|
||||||
nose
|
nose
|
||||||
nosexcover
|
nosexcover
|
||||||
|
Loading…
Reference in New Issue
Block a user