]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Santhosh/Vinkesh | Added extensions framework
authorSanthosh <santhom@thoughtworks.com>
Thu, 23 Jun 2011 12:33:59 +0000 (18:03 +0530)
committerSanthosh <santhom@thoughtworks.com>
Thu, 23 Jun 2011 12:33:59 +0000 (18:03 +0530)
etc/quantum.conf
etc/quantum.conf.sample
etc/quantum.conf.test
extensions/__init__.py [new file with mode: 0644]
quantum/common/extensions.py [new file with mode: 0644]
tests/unit/extensions/__init__.py [new file with mode: 0644]
tests/unit/extensions/foxinsocks.py [new file with mode: 0644]
tests/unit/test_extensions.py [new file with mode: 0644]

index ba96a9a275f7e1abd490d4dcc31827fd255cf249..d527c83870ca08d759ccfd53e21dc01fae29820f 100644 (file)
@@ -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
-
-
index 85d6282b5045c161db5846530c7a142ac4233d48..502503468fed4a29643d0ea3f3d35d6e74cc5670 100644 (file)
@@ -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
\ No newline at end of file
+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
index b1c266246af654711f2a53af58812afe4e7ec553..5e1e3412bcc57ce6060a36a286ff57038d2b913a 100644 (file)
@@ -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
+
diff --git a/extensions/__init__.py b/extensions/__init__.py
new file mode 100644 (file)
index 0000000..848908a
--- /dev/null
@@ -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.
diff --git a/quantum/common/extensions.py b/quantum/common/extensions.py
new file mode 100644 (file)
index 0000000..1a88d1f
--- /dev/null
@@ -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
diff --git a/tests/unit/extensions/__init__.py b/tests/unit/extensions/__init__.py
new file mode 100644 (file)
index 0000000..848908a
--- /dev/null
@@ -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.
diff --git a/tests/unit/extensions/foxinsocks.py b/tests/unit/extensions/foxinsocks.py
new file mode 100644 (file)
index 0000000..648225c
--- /dev/null
@@ -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."
diff --git a/tests/unit/test_extensions.py b/tests/unit/test_extensions.py
new file mode 100644 (file)
index 0000000..9da8d8a
--- /dev/null
@@ -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