Santhosh/Vinkesh | Added extensions framework

This commit is contained in:
Santhosh 2011-06-23 18:03:59 +05:30
parent 8b20e8270d
commit b5ff082337
8 changed files with 843 additions and 10 deletions

View File

@ -11,15 +11,22 @@ bind_host = 0.0.0.0
# Port the bind the API server to # Port the bind the API server to
bind_port = 9696 bind_port = 9696
# Path to the extensions
api_extensions_path = extensions
[composite:quantum] [composite:quantum]
use = egg:Paste#urlmap use = egg:Paste#urlmap
/: quantumversions /: quantumversions
/v0.1: quantumapi /v0.1: quantumapi
[pipeline:quantumapi]
pipeline = extensions quantumapiapp
[filter:extensions]
paste.filter_factory = quantum.common.extensions:ExtensionMiddleware.factory
[app:quantumversions] [app:quantumversions]
paste.app_factory = quantum.api.versions:Versions.factory paste.app_factory = quantum.api.versions:Versions.factory
[app:quantumapi] [app:quantumapiapp]
paste.app_factory = quantum.api:APIRouterV01.factory paste.app_factory = quantum.api:APIRouterV01.factory

View File

@ -5,11 +5,28 @@ verbose = True
# Show debugging output in logs (sets DEBUG log level output) # Show debugging output in logs (sets DEBUG log level output)
debug = False debug = False
[app:quantum]
paste.app_factory = quantum.service:app_factory
# Address to bind the API server # Address to bind the API server
bind_host = 0.0.0.0 bind_host = 0.0.0.0
# Port the bind the API server to # 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

View File

@ -5,11 +5,24 @@ verbose = True
# Show debugging output in logs (sets DEBUG log level output) # Show debugging output in logs (sets DEBUG log level output)
debug = False debug = False
[app:quantum]
paste.app_factory = quantum.l2Network.service:app_factory
# Address to bind the API server # Address to bind the API server
bind_host = 0.0.0.0 bind_host = 0.0.0.0
# Port the bind the API server to # Port the bind the API server to
bind_port = 9696 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
View 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.

View 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

View 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.

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

View 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