summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDan Prince <dan.prince@rackspace.com>2011-03-14 16:58:03 -0400
committerDan Prince <dan.prince@rackspace.com>2011-03-14 16:58:03 -0400
commit229c5bc3324d5df39ca959d71a540a806bc5ad3e (patch)
tree715d2a03aa51b114b354cf688597c2c592a2f6b6
parent2bfa7b29c7882da559041cea771b9243555828fa (diff)
downloadnova-229c5bc3324d5df39ca959d71a540a806bc5ad3e.tar.gz
nova-229c5bc3324d5df39ca959d71a540a806bc5ad3e.tar.xz
nova-229c5bc3324d5df39ca959d71a540a806bc5ad3e.zip
Implement action extensions.
-rw-r--r--etc/api-paste.ini5
-rw-r--r--nova/api/openstack/__init__.py1
-rw-r--r--nova/api/openstack/extensions.py106
-rw-r--r--nova/tests/api/openstack/extensions/widgets.py22
-rw-r--r--nova/tests/api/openstack/test_extensions.py44
5 files changed, 174 insertions, 4 deletions
diff --git a/etc/api-paste.ini b/etc/api-paste.ini
index 9f7e93d4c..c7fa62fca 100644
--- a/etc/api-paste.ini
+++ b/etc/api-paste.ini
@@ -70,7 +70,7 @@ use = egg:Paste#urlmap
/v1.0: openstackapi
[pipeline:openstackapi]
-pipeline = faultwrap auth ratelimit osapiapp
+pipeline = faultwrap auth ratelimit extensions osapiapp
[filter:faultwrap]
paste.filter_factory = nova.api.openstack:FaultWrapper.factory
@@ -81,6 +81,9 @@ paste.filter_factory = nova.api.openstack.auth:AuthMiddleware.factory
[filter:ratelimit]
paste.filter_factory = nova.api.openstack.ratelimiting:RateLimitingMiddleware.factory
+[filter:extensions]
+paste.filter_factory = nova.api.openstack.extensions:ExtensionsRouter.factory
+
[app:osapiapp]
paste.app_factory = nova.api.openstack:APIRouter.factory
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index 9b7b76a91..8a458eea1 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -124,7 +124,6 @@ class APIRouter(wsgi.Router):
if ext_mgr is None:
ext_mgr = extensions.ExtensionManager(FLAGS.osapi_extensions_path)
for resource in ext_mgr.get_resources():
- print resource
resource.add_routes(mapper)
super(APIRouter, self).__init__(mapper)
diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py
index 13789863b..e41de3120 100644
--- a/nova/api/openstack/extensions.py
+++ b/nova/api/openstack/extensions.py
@@ -18,11 +18,101 @@
import imp
import os
import sys
+import routes
+import webob.dec
+import webob.exc
+
+from nova import flags
+from nova import log as logging
+from nova import wsgi
+
+
+LOG = logging.getLogger('extensions')
+
+
+FLAGS = flags.FLAGS
+
+
+class ExtensionActionController(wsgi.Controller):
+
+ def __init__(self, application, action_name, handler):
+
+ self.application = application
+ self.action_name = action_name
+ self.handler = handler
+
+ def action(self, req, id):
+
+ input_dict = self._deserialize(req.body, req.get_content_type())
+ if self.action_name in input_dict:
+ return self.handler(input_dict, req, id)
+ # no action handler found (bump to downstream application)
+ res = self.application
+ return res
+
+
+class ExtensionMiddleware(wsgi.Middleware):
+ """
+ Extensions middleware that intercepts configured routes for extensions.
+ """
+ @classmethod
+ def factory(cls, global_config, **local_config):
+ """ paste factory """
+ def _factory(app):
+ return cls(app, **local_config)
+ return _factory
+
+ def __init__(self, application, ext_mgr=None):
+ mapper = routes.Mapper()
+
+ if ext_mgr is None:
+ ext_mgr = ExtensionManager(FLAGS.osapi_extensions_path)
+
+ # create custom mapper connections for extended actions
+ for action in ext_mgr.get_actions():
+ controller = ExtensionActionController(application, action.name,
+ action.handler)
+ 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']))
+
+ 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):
+ """
+ Called by self._router after matching the incoming request to a route
+ and putting the information into req.environ. Either 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):
def __init__(self, path):
+ LOG.audit(_('Initializing extension manager.'))
self.path = path
self.extensions = []
@@ -37,6 +127,12 @@ class ExtensionManager(object):
resources.append(ext.get_resources())
return resources
+ def get_actions(self):
+ actions = []
+ for ext in self.extensions:
+ actions.extend(ext.get_actions())
+ return actions
+
def _load_extensions(self):
"""
Load extensions from the configured path. The extension name is
@@ -48,6 +144,7 @@ class ExtensionManager(object):
return
for f in os.listdir(self.path):
+ LOG.audit(_('Loading extension file: %s'), f)
mod_name, file_ext = os.path.splitext(os.path.split(f)[-1])
ext_path = os.path.join(self.path, f)
if file_ext.lower() == '.py':
@@ -56,6 +153,15 @@ class ExtensionManager(object):
self.extensions.append(getattr(mod, ext_name)())
+class ExtensionAction(object):
+
+ def __init__(self, member, collection, name, handler):
+ self.member = member
+ self.collection = collection
+ self.name = name
+ self.handler = handler
+
+
class ExtensionResource(object):
"""
Example ExtensionResource object. All ExtensionResource objects should
diff --git a/nova/tests/api/openstack/extensions/widgets.py b/nova/tests/api/openstack/extensions/widgets.py
index e03fc7776..6839d2bb2 100644
--- a/nova/tests/api/openstack/extensions/widgets.py
+++ b/nova/tests/api/openstack/extensions/widgets.py
@@ -1,10 +1,14 @@
from nova import wsgi
+from nova.api.openstack import extensions
+
+
class WidgetsController(wsgi.Controller):
def index(self, req):
return "Buy more widgets!"
+
class WidgetsExtensionResource(object):
def __init__(self):
@@ -21,3 +25,21 @@ class WidgetsExtension(object):
def get_resources(self):
return WidgetsExtensionResource()
+
+ def get_actions(self):
+ actions = []
+ actions.append(extensions.ExtensionAction('server', 'servers',
+ 'add_widget',
+ self._add_widget))
+ actions.append(extensions.ExtensionAction('server', 'servers',
+ 'delete_widget',
+ self._delete_widget))
+ return actions
+
+ def _add_widget(self, input_dict, req, id):
+
+ return "Widget Added."
+
+ def _delete_widget(self, input_dict, req, id):
+
+ return "Widget Deleted."
diff --git a/nova/tests/api/openstack/test_extensions.py b/nova/tests/api/openstack/test_extensions.py
index ff41d6d99..f8d217e9c 100644
--- a/nova/tests/api/openstack/test_extensions.py
+++ b/nova/tests/api/openstack/test_extensions.py
@@ -15,10 +15,10 @@
# License for the specific language governing permissions and limitations
# under the License.
+import json
import unittest
-import os.path
-
import webob
+import os.path
from nova import flags
from nova.api import openstack
@@ -26,6 +26,7 @@ import nova.wsgi
FLAGS = flags.FLAGS
+
class StubController(nova.wsgi.Controller):
def __init__(self, body):
@@ -34,6 +35,7 @@ class StubController(nova.wsgi.Controller):
def index(self, req):
return self.body
+
class StubExtensionManager(object):
def __init__(self, resources):
@@ -42,6 +44,7 @@ class StubExtensionManager(object):
def get_resources(self):
return self.resources
+
class WidgetExtensionResource(object):
def __init__(self, name, collection, wsgi_app):
@@ -52,6 +55,7 @@ class WidgetExtensionResource(object):
def add_routes(self, mapper):
mapper.resource(self.name, self.collection, controller=self.wsgi_app)
+
class ExtensionTest(unittest.TestCase):
def test_no_extension_present(self):
@@ -99,4 +103,40 @@ class ExtensionManagerTest(unittest.TestCase):
self.assertEqual("Buy more widgets!", response.body)
+class ExtendedActionTest(unittest.TestCase):
+ def setUp(self):
+ FLAGS.osapi_extensions_path = os.path.join(os.path.dirname(__file__),
+ "extensions")
+
+ def test_extended_action(self):
+ app = openstack.APIRouter()
+ ext_midware = openstack.extensions.ExtensionMiddleware(app)
+ body = dict(add_widget=dict(name="test"))
+ request = webob.Request.blank("/servers/1/action")
+ request.method = 'POST'
+ request.content_type = 'application/json'
+ request.body = json.dumps(body)
+ response = request.get_response(ext_midware)
+ self.assertEqual(200, response.status_int)
+ self.assertEqual("Widget Added.", response.body)
+
+ def test_invalid_action_body(self):
+ app = openstack.APIRouter()
+ ext_midware = openstack.extensions.ExtensionMiddleware(app)
+ body = dict(blah=dict(name="test")) # Doesn't exist
+ request = webob.Request.blank("/servers/1/action")
+ request.method = 'POST'
+ request.content_type = 'application/json'
+ request.body = json.dumps(body)
+ response = request.get_response(ext_midware)
+ self.assertEqual(501, response.status_int)
+
+ def test_invalid_action(self):
+ app = openstack.APIRouter()
+ ext_midware = openstack.extensions.ExtensionMiddleware(app)
+ request = webob.Request.blank("/asdf/1/action")
+ request.method = 'POST'
+ request.content_type = 'application/json'
+ response = request.get_response(ext_midware)
+ self.assertEqual(404, response.status_int)