Santhosh/Vinkesh | Added extensions framework
This commit is contained in:
parent
8b20e8270d
commit
b5ff082337
@ -11,15 +11,22 @@ bind_host = 0.0.0.0
|
||||
# Port the bind the API server to
|
||||
bind_port = 9696
|
||||
|
||||
# Path to the extensions
|
||||
api_extensions_path = extensions
|
||||
|
||||
[composite:quantum]
|
||||
use = egg:Paste#urlmap
|
||||
/: quantumversions
|
||||
/v0.1: quantumapi
|
||||
|
||||
[pipeline:quantumapi]
|
||||
pipeline = extensions quantumapiapp
|
||||
|
||||
[filter:extensions]
|
||||
paste.filter_factory = quantum.common.extensions:ExtensionMiddleware.factory
|
||||
|
||||
[app:quantumversions]
|
||||
paste.app_factory = quantum.api.versions:Versions.factory
|
||||
|
||||
[app:quantumapi]
|
||||
[app:quantumapiapp]
|
||||
paste.app_factory = quantum.api:APIRouterV01.factory
|
||||
|
||||
|
||||
|
@ -5,11 +5,28 @@ verbose = True
|
||||
# Show debugging output in logs (sets DEBUG log level output)
|
||||
debug = False
|
||||
|
||||
[app:quantum]
|
||||
paste.app_factory = quantum.service:app_factory
|
||||
|
||||
# Address to bind the API server
|
||||
bind_host = 0.0.0.0
|
||||
|
||||
# Port the bind the API server to
|
||||
bind_port = 9696
|
||||
bind_port = 9696
|
||||
|
||||
# Path to the extensions
|
||||
api_extensions_path = extensions
|
||||
|
||||
[composite:quantum]
|
||||
use = egg:Paste#urlmap
|
||||
/: quantumversions
|
||||
/v0.1: quantumapi
|
||||
|
||||
[pipeline:quantumapi]
|
||||
pipeline = extensions quantumapiapp
|
||||
|
||||
[filter:extensions]
|
||||
paste.filter_factory = quantum.common.extensions:ExtensionMiddleware.factory
|
||||
|
||||
[app:quantumversions]
|
||||
paste.app_factory = quantum.api.versions:Versions.factory
|
||||
|
||||
[app:quantumapiapp]
|
||||
paste.app_factory = quantum.api:APIRouterV01.factory
|
||||
|
@ -5,11 +5,24 @@ verbose = True
|
||||
# Show debugging output in logs (sets DEBUG log level output)
|
||||
debug = False
|
||||
|
||||
[app:quantum]
|
||||
paste.app_factory = quantum.l2Network.service:app_factory
|
||||
|
||||
# Address to bind the API server
|
||||
bind_host = 0.0.0.0
|
||||
|
||||
# Port the bind the API server to
|
||||
bind_port = 9696
|
||||
|
||||
# Path to the extensions
|
||||
api_extensions_path = unit/extensions
|
||||
|
||||
[pipeline:extensions_app_with_filter]
|
||||
pipeline = extensions extensions_test_app
|
||||
|
||||
[filter:extensions]
|
||||
paste.filter_factory = quantum.common.extensions:ExtensionMiddleware.factory
|
||||
|
||||
[app:extensions_test_app]
|
||||
paste.app_factory = tests.unit.test_extensions:app_factory
|
||||
|
||||
[app:quantum]
|
||||
paste.app_factory = quantum.l2Network.service:app_factory
|
||||
|
||||
|
15
extensions/__init__.py
Normal file
15
extensions/__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 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.
|
440
quantum/common/extensions.py
Normal file
440
quantum/common/extensions.py
Normal file
@ -0,0 +1,440 @@
|
||||
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# Copyright 2011 Justin Santa Barbara
|
||||
# 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.
|
||||
|
||||
import imp
|
||||
import os
|
||||
import routes
|
||||
import logging
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
|
||||
from quantum.common import exceptions
|
||||
from quantum.common import wsgi
|
||||
from gettext import gettext as _
|
||||
|
||||
LOG = logging.getLogger('quantum.common.extensions')
|
||||
|
||||
|
||||
class ExtensionDescriptor(object):
|
||||
"""Base class that defines the contract for extensions.
|
||||
|
||||
Note that you don't have to derive from this class to have a valid
|
||||
extension; it is purely a convenience.
|
||||
|
||||
"""
|
||||
|
||||
def get_name(self):
|
||||
"""The name of the extension.
|
||||
|
||||
e.g. 'Fox In Socks'
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_alias(self):
|
||||
"""The alias for the extension.
|
||||
|
||||
e.g. 'FOXNSOX'
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_description(self):
|
||||
"""Friendly description for the extension.
|
||||
|
||||
e.g. 'The Fox In Socks Extension'
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_namespace(self):
|
||||
"""The XML namespace for the extension.
|
||||
|
||||
e.g. 'http://www.fox.in.socks/api/ext/pie/v1.0'
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_updated(self):
|
||||
"""The timestamp when the extension was last updated.
|
||||
|
||||
e.g. '2011-01-22T13:25:27-06:00'
|
||||
|
||||
"""
|
||||
# NOTE(justinsb): Not sure of the purpose of this is, vs the XML NS
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_resources(self):
|
||||
"""List of extensions.ResourceExtension extension objects.
|
||||
|
||||
Resources define new nouns, and are accessible through URLs.
|
||||
|
||||
"""
|
||||
resources = []
|
||||
return resources
|
||||
|
||||
def get_actions(self):
|
||||
"""List of extensions.ActionExtension extension objects.
|
||||
|
||||
Actions are verbs callable from the API.
|
||||
|
||||
"""
|
||||
actions = []
|
||||
return actions
|
||||
|
||||
def get_request_extensions(self):
|
||||
"""List of extensions.RequestException extension objects.
|
||||
|
||||
Request extensions are used to handle custom request data.
|
||||
|
||||
"""
|
||||
request_exts = []
|
||||
return request_exts
|
||||
|
||||
|
||||
class ActionExtensionController(wsgi.Controller):
|
||||
|
||||
def __init__(self, application):
|
||||
|
||||
self.application = application
|
||||
self.action_handlers = {}
|
||||
|
||||
def add_action(self, action_name, handler):
|
||||
self.action_handlers[action_name] = handler
|
||||
|
||||
def action(self, request, id):
|
||||
|
||||
input_dict = self._deserialize(request.body,
|
||||
request.get_content_type())
|
||||
for action_name, handler in self.action_handlers.iteritems():
|
||||
if action_name in input_dict:
|
||||
return handler(input_dict, request, id)
|
||||
# no action handler found (bump to downstream application)
|
||||
response = self.application
|
||||
return response
|
||||
|
||||
|
||||
class RequestExtensionController(wsgi.Controller):
|
||||
|
||||
def __init__(self, application):
|
||||
self.application = application
|
||||
self.handlers = []
|
||||
|
||||
def add_handler(self, handler):
|
||||
self.handlers.append(handler)
|
||||
|
||||
def process(self, request, *args, **kwargs):
|
||||
res = request.get_response(self.application)
|
||||
# currently request handlers are un-ordered
|
||||
for handler in self.handlers:
|
||||
response = handler(request, res)
|
||||
return response
|
||||
|
||||
|
||||
class ExtensionController(wsgi.Controller):
|
||||
|
||||
def __init__(self, extension_manager):
|
||||
self.extension_manager = extension_manager
|
||||
|
||||
def _translate(self, ext):
|
||||
ext_data = {}
|
||||
ext_data['name'] = ext.get_name()
|
||||
ext_data['alias'] = ext.get_alias()
|
||||
ext_data['description'] = ext.get_description()
|
||||
ext_data['namespace'] = ext.get_namespace()
|
||||
ext_data['updated'] = ext.get_updated()
|
||||
ext_data['links'] = [] # TODO(dprince): implement extension links
|
||||
return ext_data
|
||||
|
||||
def index(self, request):
|
||||
extensions = []
|
||||
for _alias, ext in self.extension_manager.extensions.iteritems():
|
||||
extensions.append(self._translate(ext))
|
||||
return dict(extensions=extensions)
|
||||
|
||||
def show(self, request, id):
|
||||
# NOTE(dprince): the extensions alias is used as the 'id' for show
|
||||
ext = self.extension_manager.extensions[id]
|
||||
return self._translate(ext)
|
||||
|
||||
def delete(self, request, id):
|
||||
raise webob.exc.HTTPNotFound()
|
||||
|
||||
def create(self, request):
|
||||
raise webob.exc.HTTPNotFound()
|
||||
|
||||
|
||||
class ExtensionMiddleware(wsgi.Middleware):
|
||||
"""Extensions middleware for WSGI."""
|
||||
@classmethod
|
||||
def factory(cls, global_config, **local_config):
|
||||
"""Paste factory."""
|
||||
def _factory(app):
|
||||
return cls(app, global_config, **local_config)
|
||||
return _factory
|
||||
|
||||
def _action_ext_controllers(self, application, ext_mgr, mapper):
|
||||
"""Return a dict of ActionExtensionController-s by collection."""
|
||||
action_controllers = {}
|
||||
for action in ext_mgr.get_actions():
|
||||
if not action.collection in action_controllers.keys():
|
||||
controller = ActionExtensionController(application)
|
||||
mapper.connect("/%s/:(id)/action.:(format)" %
|
||||
action.collection,
|
||||
action='action',
|
||||
controller=controller,
|
||||
conditions=dict(method=['POST']))
|
||||
mapper.connect("/%s/:(id)/action" % action.collection,
|
||||
action='action',
|
||||
controller=controller,
|
||||
conditions=dict(method=['POST']))
|
||||
action_controllers[action.collection] = controller
|
||||
|
||||
return action_controllers
|
||||
|
||||
def _request_ext_controllers(self, application, ext_mgr, mapper):
|
||||
"""Returns a dict of RequestExtensionController-s by collection."""
|
||||
request_ext_controllers = {}
|
||||
for req_ext in ext_mgr.get_request_extensions():
|
||||
if not req_ext.key in request_ext_controllers.keys():
|
||||
controller = RequestExtensionController(application)
|
||||
mapper.connect(req_ext.url_route + '.:(format)',
|
||||
action='process',
|
||||
controller=controller,
|
||||
conditions=req_ext.conditions)
|
||||
|
||||
mapper.connect(req_ext.url_route,
|
||||
action='process',
|
||||
controller=controller,
|
||||
conditions=req_ext.conditions)
|
||||
request_ext_controllers[req_ext.key] = controller
|
||||
|
||||
return request_ext_controllers
|
||||
|
||||
def __init__(self, application, config_params,
|
||||
ext_mgr=None):
|
||||
|
||||
self.ext_mgr = (ext_mgr
|
||||
or ExtensionManager(config_params.get('api_extensions_path',
|
||||
'')))
|
||||
|
||||
mapper = routes.Mapper()
|
||||
|
||||
# extended resources
|
||||
for resource in self.ext_mgr.get_resources():
|
||||
LOG.debug(_('Extended resource: %s'),
|
||||
resource.collection)
|
||||
mapper.resource(resource.collection, resource.collection,
|
||||
controller=resource.controller,
|
||||
collection=resource.collection_actions,
|
||||
member=resource.member_actions,
|
||||
parent_resource=resource.parent)
|
||||
|
||||
# extended actions
|
||||
action_controllers = self._action_ext_controllers(application,
|
||||
self.ext_mgr, mapper)
|
||||
for action in self.ext_mgr.get_actions():
|
||||
LOG.debug(_('Extended action: %s'), action.action_name)
|
||||
controller = action_controllers[action.collection]
|
||||
controller.add_action(action.action_name, action.handler)
|
||||
|
||||
# extended requests
|
||||
req_controllers = self._request_ext_controllers(application,
|
||||
self.ext_mgr, mapper)
|
||||
for request_ext in self.ext_mgr.get_request_extensions():
|
||||
LOG.debug(_('Extended request: %s'), request_ext.key)
|
||||
controller = req_controllers[request_ext.key]
|
||||
controller.add_handler(request_ext.handler)
|
||||
|
||||
self._router = routes.middleware.RoutesMiddleware(self._dispatch,
|
||||
mapper)
|
||||
|
||||
super(ExtensionMiddleware, self).__init__(application)
|
||||
|
||||
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
||||
def __call__(self, req):
|
||||
"""Route the incoming request with router."""
|
||||
req.environ['extended.app'] = self.application
|
||||
return self._router
|
||||
|
||||
@staticmethod
|
||||
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
||||
def _dispatch(req):
|
||||
"""Dispatch the request.
|
||||
|
||||
Returns the routed WSGI app's response or defers to the extended
|
||||
application.
|
||||
|
||||
"""
|
||||
match = req.environ['wsgiorg.routing_args'][1]
|
||||
if not match:
|
||||
return req.environ['extended.app']
|
||||
app = match['controller']
|
||||
return app
|
||||
|
||||
|
||||
class ExtensionManager(object):
|
||||
"""Load extensions from the configured extension path.
|
||||
|
||||
See tests/unit/extensions/foxinsocks.py for an
|
||||
example extension implementation.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, path):
|
||||
LOG.info(_('Initializing extension manager.'))
|
||||
|
||||
self.path = path
|
||||
self.extensions = {}
|
||||
self._load_all_extensions()
|
||||
|
||||
def get_resources(self):
|
||||
"""Returns a list of ResourceExtension objects."""
|
||||
resources = []
|
||||
resources.append(ResourceExtension('extensions',
|
||||
ExtensionController(self)))
|
||||
for alias, ext in self.extensions.iteritems():
|
||||
try:
|
||||
resources.extend(ext.get_resources())
|
||||
except AttributeError:
|
||||
# NOTE(dprince): Extension aren't required to have resource
|
||||
# extensions
|
||||
pass
|
||||
return resources
|
||||
|
||||
def get_actions(self):
|
||||
"""Returns a list of ActionExtension objects."""
|
||||
actions = []
|
||||
for alias, ext in self.extensions.iteritems():
|
||||
try:
|
||||
actions.extend(ext.get_actions())
|
||||
except AttributeError:
|
||||
# NOTE(dprince): Extension aren't required to have action
|
||||
# extensions
|
||||
pass
|
||||
return actions
|
||||
|
||||
def get_request_extensions(self):
|
||||
"""Returns a list of RequestExtension objects."""
|
||||
request_exts = []
|
||||
for alias, ext in self.extensions.iteritems():
|
||||
try:
|
||||
request_exts.extend(ext.get_request_extensions())
|
||||
except AttributeError:
|
||||
# NOTE(dprince): Extension aren't required to have request
|
||||
# extensions
|
||||
pass
|
||||
return request_exts
|
||||
|
||||
def _check_extension(self, extension):
|
||||
"""Checks for required methods in extension objects."""
|
||||
try:
|
||||
LOG.debug(_('Ext name: %s'), extension.get_name())
|
||||
LOG.debug(_('Ext alias: %s'), extension.get_alias())
|
||||
LOG.debug(_('Ext description: %s'), extension.get_description())
|
||||
LOG.debug(_('Ext namespace: %s'), extension.get_namespace())
|
||||
LOG.debug(_('Ext updated: %s'), extension.get_updated())
|
||||
except AttributeError as ex:
|
||||
LOG.exception(_("Exception loading extension: %s"), unicode(ex))
|
||||
|
||||
def _load_all_extensions(self):
|
||||
"""Load extensions from the configured path.
|
||||
|
||||
Load extensions from the configured path. The extension name is
|
||||
constructed from the module_name. If your extension module was named
|
||||
widgets.py the extension class within that module should be
|
||||
'Widgets'.
|
||||
|
||||
In addition, extensions are loaded from the 'contrib' directory.
|
||||
|
||||
See tests/unit/extensions/foxinsocks.py for an example
|
||||
extension implementation.
|
||||
|
||||
"""
|
||||
if os.path.exists(self.path):
|
||||
self._load_all_extensions_from_path(self.path)
|
||||
|
||||
contrib_path = os.path.join(os.path.dirname(__file__), "contrib")
|
||||
if os.path.exists(contrib_path):
|
||||
self._load_all_extensions_from_path(contrib_path)
|
||||
|
||||
def _load_all_extensions_from_path(self, path):
|
||||
for f in os.listdir(path):
|
||||
LOG.info(_('Loading extension file: %s'), f)
|
||||
mod_name, file_ext = os.path.splitext(os.path.split(f)[-1])
|
||||
ext_path = os.path.join(path, f)
|
||||
if file_ext.lower() == '.py' and not mod_name.startswith('_'):
|
||||
mod = imp.load_source(mod_name, ext_path)
|
||||
ext_name = mod_name[0].upper() + mod_name[1:]
|
||||
new_ext_class = getattr(mod, ext_name, None)
|
||||
if not new_ext_class:
|
||||
LOG.warn(_('Did not find expected name '
|
||||
'"%(ext_name)s" in %(file)s'),
|
||||
{'ext_name': ext_name,
|
||||
'file': ext_path})
|
||||
continue
|
||||
new_ext = new_ext_class()
|
||||
self._check_extension(new_ext)
|
||||
self._add_extension(new_ext)
|
||||
|
||||
def _add_extension(self, ext):
|
||||
alias = ext.get_alias()
|
||||
LOG.info(_('Loaded extension: %s'), alias)
|
||||
|
||||
self._check_extension(ext)
|
||||
|
||||
if alias in self.extensions:
|
||||
raise exception.Error("Found duplicate extension: %s"
|
||||
% alias)
|
||||
self.extensions[alias] = ext
|
||||
|
||||
|
||||
class RequestExtension(object):
|
||||
"""Extend requests and responses of core Quantum OpenStack API controllers.
|
||||
|
||||
Provide a way to add data to responses and handle custom request data
|
||||
that is sent to core Quantum OpenStack API controllers.
|
||||
|
||||
"""
|
||||
def __init__(self, method, url_route, handler):
|
||||
self.url_route = url_route
|
||||
self.handler = handler
|
||||
self.conditions = dict(method=[method])
|
||||
self.key = "%s-%s" % (method, url_route)
|
||||
|
||||
|
||||
class ActionExtension(object):
|
||||
"""Add custom actions to core Quantum OpenStack API controllers."""
|
||||
|
||||
def __init__(self, collection, action_name, handler):
|
||||
self.collection = collection
|
||||
self.action_name = action_name
|
||||
self.handler = handler
|
||||
|
||||
|
||||
class ResourceExtension(object):
|
||||
"""Add top level resources to the OpenStack API in Quantum."""
|
||||
|
||||
def __init__(self, collection, controller, parent=None,
|
||||
collection_actions={}, member_actions={}):
|
||||
self.collection = collection
|
||||
self.controller = controller
|
||||
self.parent = parent
|
||||
self.collection_actions = collection_actions
|
||||
self.member_actions = member_actions
|
15
tests/unit/extensions/__init__.py
Normal file
15
tests/unit/extensions/__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 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.
|
97
tests/unit/extensions/foxinsocks.py
Normal file
97
tests/unit/extensions/foxinsocks.py
Normal file
@ -0,0 +1,97 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 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.
|
||||
|
||||
import json
|
||||
|
||||
from quantum.common import wsgi
|
||||
from quantum.common import extensions
|
||||
|
||||
|
||||
class FoxInSocksController(wsgi.Controller):
|
||||
|
||||
def index(self, request):
|
||||
return "Try to say this Mr. Knox, sir..."
|
||||
|
||||
|
||||
class Foxinsocks(object):
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def get_name(self):
|
||||
return "Fox In Socks"
|
||||
|
||||
def get_alias(self):
|
||||
return "FOXNSOX"
|
||||
|
||||
def get_description(self):
|
||||
return "The Fox In Socks Extension"
|
||||
|
||||
def get_namespace(self):
|
||||
return "http://www.fox.in.socks/api/ext/pie/v1.0"
|
||||
|
||||
def get_updated(self):
|
||||
return "2011-01-22T13:25:27-06:00"
|
||||
|
||||
def get_resources(self):
|
||||
resources = []
|
||||
resource = extensions.ResourceExtension('foxnsocks',
|
||||
FoxInSocksController())
|
||||
resources.append(resource)
|
||||
return resources
|
||||
|
||||
def get_actions(self):
|
||||
return [extensions.ActionExtension('dummy_resources', 'add_tweedle',
|
||||
self._add_tweedle),
|
||||
extensions.ActionExtension('dummy_resources',
|
||||
'delete_tweedle', self._delete_tweedle)]
|
||||
|
||||
def get_request_extensions(self):
|
||||
request_exts = []
|
||||
|
||||
def _goose_handler(req, res):
|
||||
#NOTE: This only handles JSON responses.
|
||||
# You can use content type header to test for XML.
|
||||
data = json.loads(res.body)
|
||||
data['googoose'] = req.GET.get('chewing')
|
||||
res.body = json.dumps(data)
|
||||
return res
|
||||
|
||||
req_ext1 = extensions.RequestExtension('GET', '/dummy_resources/:(id)',
|
||||
_goose_handler)
|
||||
request_exts.append(req_ext1)
|
||||
|
||||
def _bands_handler(req, res):
|
||||
#NOTE: This only handles JSON responses.
|
||||
# You can use content type header to test for XML.
|
||||
data = json.loads(res.body)
|
||||
data['big_bands'] = 'Pig Bands!'
|
||||
res.body = json.dumps(data)
|
||||
return res
|
||||
|
||||
req_ext2 = extensions.RequestExtension('GET', '/dummy_resources/:(id)',
|
||||
_bands_handler)
|
||||
request_exts.append(req_ext2)
|
||||
return request_exts
|
||||
|
||||
def _add_tweedle(self, input_dict, req, id):
|
||||
|
||||
return "Tweedle Beetle Added."
|
||||
|
||||
def _delete_tweedle(self, input_dict, req, id):
|
||||
|
||||
return "Tweedle Beetle Deleted."
|
229
tests/unit/test_extensions.py
Normal file
229
tests/unit/test_extensions.py
Normal file
@ -0,0 +1,229 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
# Copyright 2011 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.
|
||||
import json
|
||||
import unittest
|
||||
import routes
|
||||
import os.path
|
||||
from tests.unit import BaseTest
|
||||
|
||||
from webtest import TestApp
|
||||
from quantum.common import extensions
|
||||
from quantum.common import wsgi
|
||||
from quantum.common import config
|
||||
|
||||
|
||||
response_body = "Try to say this Mr. Knox, sir..."
|
||||
test_conf_file = os.path.join(os.path.dirname(__file__), os.pardir,
|
||||
os.pardir, 'etc', 'quantum.conf.test')
|
||||
|
||||
|
||||
class ExtensionControllerTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ExtensionControllerTest, self).setUp()
|
||||
self.test_app = setup_extensions_test_app()
|
||||
|
||||
def test_index(self):
|
||||
response = self.test_app.get("/extensions")
|
||||
self.assertEqual(200, response.status_int)
|
||||
|
||||
def test_get_by_alias(self):
|
||||
response = self.test_app.get("/extensions/FOXNSOX")
|
||||
self.assertEqual(200, response.status_int)
|
||||
|
||||
|
||||
class ResourceExtensionTest(unittest.TestCase):
|
||||
|
||||
def test_no_extension_present(self):
|
||||
test_app = setup_extensions_test_app(StubExtensionManager(None))
|
||||
response = test_app.get("/blah", status='*')
|
||||
self.assertEqual(404, response.status_int)
|
||||
|
||||
def test_get_resources(self):
|
||||
res_ext = extensions.ResourceExtension('tweedles',
|
||||
StubController(response_body))
|
||||
test_app = setup_extensions_test_app(StubExtensionManager(res_ext))
|
||||
|
||||
response = test_app.get("/tweedles")
|
||||
self.assertEqual(200, response.status_int)
|
||||
self.assertEqual(response_body, response.body)
|
||||
|
||||
|
||||
class ExtensionManagerTest(unittest.TestCase):
|
||||
|
||||
def test_get_resources(self):
|
||||
test_app = setup_extensions_test_app()
|
||||
response = test_app.get('/foxnsocks')
|
||||
|
||||
self.assertEqual(200, response.status_int)
|
||||
self.assertEqual(response_body, response.body)
|
||||
|
||||
|
||||
class ActionExtensionTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ActionExtensionTest, self).setUp()
|
||||
self.test_app = setup_extensions_test_app()
|
||||
|
||||
def _send_server_action_request(self, url, body):
|
||||
return self.test_app.post(url, json.dumps(body),
|
||||
content_type='application/json', status='*')
|
||||
|
||||
def test_extended_action(self):
|
||||
body = json.dumps(dict(add_tweedle=dict(name="test")))
|
||||
response = self.test_app.post('/dummy_resources/1/action', body,
|
||||
content_type='application/json')
|
||||
self.assertEqual("Tweedle Beetle Added.", response.body)
|
||||
|
||||
body = json.dumps(dict(delete_tweedle=dict(name="test")))
|
||||
response = self.test_app.post("/dummy_resources/1/action", body,
|
||||
content_type='application/json')
|
||||
|
||||
self.assertEqual(200, response.status_int)
|
||||
self.assertEqual("Tweedle Beetle Deleted.", response.body)
|
||||
|
||||
def test_invalid_action_body(self):
|
||||
body = json.dumps(dict(blah=dict(name="test"))) # Doesn't exist
|
||||
response = self.test_app.post("/dummy_resources/1/action", body,
|
||||
content_type='application/json',
|
||||
status='*')
|
||||
self.assertEqual(404, response.status_int)
|
||||
|
||||
def test_invalid_action(self):
|
||||
body = json.dumps(dict(blah=dict(name="test")))
|
||||
response = self.test_app.post("/asdf/1/action",
|
||||
body, content_type='application/json',
|
||||
status='*')
|
||||
self.assertEqual(404, response.status_int)
|
||||
|
||||
|
||||
class RequestExtensionTest(BaseTest):
|
||||
|
||||
def test_get_resources_with_stub_mgr(self):
|
||||
|
||||
def _req_handler(req, res):
|
||||
# only handle JSON responses
|
||||
data = json.loads(res.body)
|
||||
data['googoose'] = req.GET.get('chewing')
|
||||
res.body = json.dumps(data)
|
||||
return res
|
||||
|
||||
req_ext = extensions.RequestExtension('GET',
|
||||
'/dummy_resources/:(id)',
|
||||
_req_handler)
|
||||
|
||||
manager = StubExtensionManager(None, None, req_ext)
|
||||
app = setup_extensions_test_app(manager)
|
||||
|
||||
response = app.get("/dummy_resources/1?chewing=bluegoos",
|
||||
extra_environ={'api.version': '1.1'})
|
||||
|
||||
self.assertEqual(200, response.status_int)
|
||||
response_data = json.loads(response.body)
|
||||
self.assertEqual('bluegoos', response_data['googoose'])
|
||||
self.assertEqual('knox', response_data['fort'])
|
||||
|
||||
def test_get_resources_with_mgr(self):
|
||||
app = setup_extensions_test_app()
|
||||
|
||||
response = app.get("/dummy_resources/1?"
|
||||
"chewing=newblue", status='*')
|
||||
|
||||
self.assertEqual(200, response.status_int)
|
||||
response_data = json.loads(response.body)
|
||||
self.assertEqual('newblue', response_data['googoose'])
|
||||
self.assertEqual("Pig Bands!", response_data['big_bands'])
|
||||
|
||||
|
||||
class TestExtensionMiddlewareFactory(unittest.TestCase):
|
||||
|
||||
def test_app_configured_with_extensions_as_filter(self):
|
||||
conf, quantum_app = config.load_paste_app('extensions_app_with_filter',
|
||||
{"config_file": test_conf_file},
|
||||
None)
|
||||
|
||||
response = TestApp(quantum_app).get("/extensions")
|
||||
self.assertEqual(response.status_int, 200)
|
||||
|
||||
|
||||
class ExtensionsTestApp(wsgi.Router):
|
||||
|
||||
def __init__(self, options={}):
|
||||
mapper = routes.Mapper()
|
||||
controller = StubController(response_body)
|
||||
mapper.resource("dummy_resource", "/dummy_resources",
|
||||
controller=controller)
|
||||
super(ExtensionsTestApp, self).__init__(mapper)
|
||||
|
||||
|
||||
class StubController(wsgi.Controller):
|
||||
|
||||
def __init__(self, body):
|
||||
self.body = body
|
||||
|
||||
def index(self, request):
|
||||
return self.body
|
||||
|
||||
def show(self, request, id):
|
||||
return {'fort': 'knox'}
|
||||
|
||||
|
||||
def app_factory(global_conf, **local_conf):
|
||||
conf = global_conf.copy()
|
||||
conf.update(local_conf)
|
||||
return ExtensionsTestApp(conf)
|
||||
|
||||
|
||||
def setup_extensions_test_app(extension_manager=None):
|
||||
options = {'config_file': test_conf_file}
|
||||
conf, app = config.load_paste_app('extensions_test_app', options, None)
|
||||
extended_app = extensions.ExtensionMiddleware(app, conf, extension_manager)
|
||||
return TestApp(extended_app)
|
||||
|
||||
|
||||
class StubExtensionManager(object):
|
||||
|
||||
def __init__(self, resource_ext=None, action_ext=None, request_ext=None):
|
||||
self.resource_ext = resource_ext
|
||||
self.action_ext = action_ext
|
||||
self.request_ext = request_ext
|
||||
|
||||
def get_name(self):
|
||||
return "Tweedle Beetle Extension"
|
||||
|
||||
def get_alias(self):
|
||||
return "TWDLBETL"
|
||||
|
||||
def get_description(self):
|
||||
return "Provides access to Tweedle Beetles"
|
||||
|
||||
def get_resources(self):
|
||||
resource_exts = []
|
||||
if self.resource_ext:
|
||||
resource_exts.append(self.resource_ext)
|
||||
return resource_exts
|
||||
|
||||
def get_actions(self):
|
||||
action_exts = []
|
||||
if self.action_ext:
|
||||
action_exts.append(self.action_ext)
|
||||
return action_exts
|
||||
|
||||
def get_request_extensions(self):
|
||||
request_extensions = []
|
||||
if self.request_ext:
|
||||
request_extensions.append(self.request_ext)
|
||||
return request_extensions
|
Loading…
Reference in New Issue
Block a user