summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDan Prince <dan.prince@rackspace.com>2011-03-15 23:00:09 -0400
committerDan Prince <dan.prince@rackspace.com>2011-03-15 23:00:09 -0400
commit0053b776276e9cac617c812931c248be7e49fea2 (patch)
treea45617e77dbb9a009d3aabb44543f7f93f3799e1
parentf0141b1616e1b1fc9e52e33b37cc3a1091c57587 (diff)
Add ResponseExtensions.
-rw-r--r--nova/api/openstack/extensions.py144
-rw-r--r--nova/tests/api/openstack/extensions/widgets.py26
-rw-r--r--nova/tests/api/openstack/test_extensions.py103
3 files changed, 213 insertions, 60 deletions
diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py
index f32471051..8b8806c8a 100644
--- a/nova/api/openstack/extensions.py
+++ b/nova/api/openstack/extensions.py
@@ -33,24 +33,43 @@ LOG = logging.getLogger('extensions')
FLAGS = flags.FLAGS
-class ExtensionActionController(wsgi.Controller):
+class ActionExtensionController(wsgi.Controller):
- def __init__(self, application, action_name, handler):
+ def __init__(self, application):
self.application = application
- self.action_name = action_name
- self.handler = handler
+ self.action_handlers = {}
+
+ def add_action(self, action_name, handler):
+ self.action_handlers[action_name] = 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)
+ for action_name, handler in self.action_handlers.iteritems():
+ if action_name in input_dict:
+ return handler(input_dict, req, id)
# no action handler found (bump to downstream application)
res = self.application
return res
+class ResponseExtensionController(wsgi.Controller):
+
+ def __init__(self, application):
+ self.application = application
+ self.handlers = []
+
+ def add_handler(self, handler):
+ self.handlers.append(handler)
+
+ def process(self, req, *args, **kwargs):
+ res = req.get_response(self.application)
+ # currently response handlers are un-ordered
+ for handler in self.handlers:
+ return handler(res)
+
+
class ExtensionMiddleware(wsgi.Middleware):
"""
Extensions middleware that intercepts configured routes for extensions.
@@ -62,33 +81,80 @@ class ExtensionMiddleware(wsgi.Middleware):
return cls(app, **local_config)
return _factory
+ def _actions_by_collection(self, application, ext_mgr):
+ """
+ Return a dict of ActionExtensionController objects by collection
+ """
+ action_controllers = {}
+ for action in ext_mgr.get_actions():
+ if not action.collection in action_controllers.keys():
+ controller = ActionExtensionController(application)
+ action_controllers[action.collection] = controller
+ return action_controllers
+
+ def _responses_by_collection(self, application, ext_mgr):
+ """
+ Return a dict of ResponseExtensionController objects by collection
+ """
+ response_ext_controllers = {}
+ for resp_ext in ext_mgr.get_response_extensions():
+ if not resp_ext.url_route in response_ext_controllers.keys():
+ controller = ResponseExtensionController(application)
+ response_ext_controllers[resp_ext.url_route] = controller
+ return response_ext_controllers
+
def __init__(self, application, ext_mgr=None):
- mapper = routes.Mapper()
if ext_mgr is None:
ext_mgr = ExtensionManager(FLAGS.osapi_extensions_path)
+ self.ext_mgr = ext_mgr
+
+ mapper = routes.Mapper()
# extended resources
for resource in ext_mgr.get_resources():
- mapper.resource(resource.member, resource.collection,
+ 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._actions_by_collection(application, ext_mgr)
for action in ext_mgr.get_actions():
- controller = ExtensionActionController(application, action.name,
- action.handler)
- mapper.connect("/%s/{id}/action.:(format)" % action.collection,
+ LOG.debug(_('Extended collection/action: %s/%s'),
+ action.collection,
+ action.action_name)
+ controller = action_controllers[action.collection]
+ controller.add_action(action.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,
+ mapper.connect("/%s/:(id)/action" % action.collection,
action='action',
controller=controller,
conditions=dict(method=['POST']))
+ # extended responses
+ resp_controllers = self._responses_by_collection(application, ext_mgr)
+ for response_ext in ext_mgr.get_response_extensions():
+ LOG.debug(_('Extended response: %s'), response_ext.url_route)
+ controller = resp_controllers[response_ext.url_route]
+ controller.add_handler(response_ext.handler)
+ mapper.connect(response_ext.url_route + '.:(format)',
+ action='process',
+ controller=controller,
+ conditions=response_ext.conditions)
+
+ mapper.connect(response_ext.url_route,
+ action='process',
+ controller=controller,
+ conditions=response_ext.conditions)
+
self._router = routes.middleware.RoutesMiddleware(self._dispatch,
mapper)
@@ -106,9 +172,8 @@ class ExtensionMiddleware(wsgi.Middleware):
@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.
+ Returns the routed WSGI app's response or defers to the extended
+ application.
"""
match = req.environ['wsgiorg.routing_args'][1]
if not match:
@@ -128,7 +193,7 @@ class ExtensionManager(object):
def get_resources(self):
"""
- returns a list of ExtensionResource objects
+ returns a list of ResourceExtension objects
"""
resources = []
for ext in self.extensions:
@@ -137,13 +202,22 @@ class ExtensionManager(object):
def get_actions(self):
"""
- returns a list of ExtensionAction objects
+ returns a list of ActionExtension objects
"""
actions = []
for ext in self.extensions:
actions.extend(ext.get_actions())
return actions
+ def get_response_extensions(self):
+ """
+ returns a list of ResponseExtension objects
+ """
+ response_exts = []
+ for ext in self.extensions:
+ response_exts.extend(ext.get_response_extensions())
+ return response_exts
+
def _load_extensions(self):
"""
Load extensions from the configured path. The extension name is
@@ -160,24 +234,42 @@ class ExtensionManager(object):
ext_path = os.path.join(self.path, f)
if file_ext.lower() == '.py':
mod = imp.load_source(mod_name, ext_path)
- ext_name = mod_name[0].upper() + mod_name[1:] + 'Extension'
+ ext_name = mod_name[0].upper() + mod_name[1:]
self.extensions.append(getattr(mod, ext_name)())
-class ExtensionAction(object):
+class ResponseExtension(object):
+ """
+ ResponseExtension objects can be used to add data to responses from
+ core nova OpenStack API controllers.
+ """
- def __init__(self, member, collection, name, handler):
- self.member = member
+ def __init__(self, url_route, method, handler):
+ self.url_route = url_route
+ self.conditions = dict(method=[method])
+ self.handler = handler
+
+
+class ActionExtension(object):
+ """
+ ActionExtension objects can be used to add custom actions to core nova
+ nova OpenStack API controllers.
+ """
+
+ def __init__(self, collection, action_name, handler):
self.collection = collection
- self.name = name
+ self.action_name = action_name
self.handler = handler
-class ExtensionResource(object):
+class ResourceExtension(object):
+ """
+ ResourceExtension objects can be used to add add top level resources
+ to the OpenStack API in nova.
+ """
- def __init__(self, member, collection, controller,
- parent=None, collection_actions={}, member_actions={}):
- self.member = member
+ def __init__(self, collection, controller, parent=None,
+ collection_actions={}, member_actions={}):
self.collection = collection
self.controller = controller
self.parent = parent
diff --git a/nova/tests/api/openstack/extensions/widgets.py b/nova/tests/api/openstack/extensions/widgets.py
index d5a2d95d9..f463721f1 100644
--- a/nova/tests/api/openstack/extensions/widgets.py
+++ b/nova/tests/api/openstack/extensions/widgets.py
@@ -1,3 +1,5 @@
+import json
+
from nova import wsgi
from nova.api.openstack import extensions
@@ -9,28 +11,40 @@ class WidgetsController(wsgi.Controller):
return "Buy more widgets!"
-class WidgetsExtension(object):
+class Widgets(object):
def __init__(self):
pass
def get_resources(self):
resources = []
- widgets = extensions.ExtensionResource('widget', 'widgets',
+ widgets = extensions.ResourceExtension('widgets',
WidgetsController())
resources.append(widgets)
return resources
def get_actions(self):
actions = []
- actions.append(extensions.ExtensionAction('server', 'servers',
- 'add_widget',
+ actions.append(extensions.ActionExtension('servers', 'add_widget',
self._add_widget))
- actions.append(extensions.ExtensionAction('server', 'servers',
- 'delete_widget',
+ actions.append(extensions.ActionExtension('servers', 'delete_widget',
self._delete_widget))
return actions
+ def get_response_extensions(self):
+ response_exts = []
+
+ def _resp_handler(res):
+ # only handle JSON responses
+ data = json.loads(res.body)
+ data['flavor']['widgets'] = "Buy more widgets!"
+ return data
+
+ widgets = extensions.ResponseExtension('/v1.0/flavors/:(id)', 'GET',
+ _resp_handler)
+ response_exts.append(widgets)
+ return response_exts
+
def _add_widget(self, input_dict, req, id):
return "Widget Added."
diff --git a/nova/tests/api/openstack/test_extensions.py b/nova/tests/api/openstack/test_extensions.py
index 080760c14..8725c8f0e 100644
--- a/nova/tests/api/openstack/test_extensions.py
+++ b/nova/tests/api/openstack/test_extensions.py
@@ -16,13 +16,17 @@
# under the License.
import json
+import stubout
import unittest
import webob
import os.path
+from nova import context
from nova import flags
from nova.api import openstack
from nova.api.openstack import extensions
+from nova.api.openstack import flavors
+from nova.tests.api.openstack import fakes
import nova.wsgi
FLAGS = flags.FLAGS
@@ -39,21 +43,31 @@ class StubController(nova.wsgi.Controller):
class StubExtensionManager(object):
- def __init__(self, resource):
- self.resource = resource
+ def __init__(self, resource_ext=None, action_ext=None, response_ext=None):
+ self.resource_ext = resource_ext
+ self.action_ext = action_ext
+ self.response_ext = response_ext
def get_resources(self):
- resources = []
- if self.resource:
- resources.append(self.resource)
- return resources
+ resource_exts = []
+ if self.resource_ext:
+ resource_exts.append(self.resource_ext)
+ return resource_exts
def get_actions(self):
- actions = []
- return actions
+ action_exts = []
+ if self.action_ext:
+ action_exts.append(self.action_ext)
+ return action_exts
+ def get_response_extensions(self):
+ response_exts = []
+ if self.response_ext:
+ response_exts.append(self.response_ext)
+ return response_exts
-class ExtensionResourceTest(unittest.TestCase):
+
+class ResourceExtensionTest(unittest.TestCase):
def test_no_extension_present(self):
manager = StubExtensionManager(None)
@@ -65,7 +79,7 @@ class ExtensionResourceTest(unittest.TestCase):
def test_get_resources(self):
response_body = "Buy more widgets!"
- widgets = extensions.ExtensionResource('widget', 'widgets',
+ widgets = extensions.ResourceExtension('widgets',
StubController(response_body))
manager = StubExtensionManager(widgets)
app = openstack.APIRouter()
@@ -77,7 +91,7 @@ class ExtensionResourceTest(unittest.TestCase):
def test_get_resources_with_controller(self):
response_body = "Buy more widgets!"
- widgets = extensions.ExtensionResource('widget', 'widgets',
+ widgets = extensions.ResourceExtension('widgets',
StubController(response_body))
manager = StubExtensionManager(widgets)
app = openstack.APIRouter()
@@ -103,40 +117,73 @@ class ExtensionManagerTest(unittest.TestCase):
self.assertEqual("Buy more widgets!", response.body)
-class ExtendedActionTest(unittest.TestCase):
+class ActionExtensionTest(unittest.TestCase):
def setUp(self):
FLAGS.osapi_extensions_path = os.path.join(os.path.dirname(__file__),
"extensions")
- def test_extended_action(self):
+ def _send_server_action_request(self, url, body):
app = openstack.APIRouter()
ext_midware = extensions.ExtensionMiddleware(app)
- body = dict(add_widget=dict(name="test"))
- request = webob.Request.blank("/servers/1/action")
+ request = webob.Request.blank(url)
request.method = 'POST'
request.content_type = 'application/json'
request.body = json.dumps(body)
response = request.get_response(ext_midware)
+ return response
+
+ def test_extended_action(self):
+ body = dict(add_widget=dict(name="test"))
+ response = self._send_server_action_request("/servers/1/action", body)
self.assertEqual(200, response.status_int)
self.assertEqual("Widget Added.", response.body)
+ body = dict(delete_widget=dict(name="test"))
+ response = self._send_server_action_request("/servers/1/action", body)
+ self.assertEqual(200, response.status_int)
+ self.assertEqual("Widget Deleted.", response.body)
+
def test_invalid_action_body(self):
- app = openstack.APIRouter()
- ext_midware = 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)
+ response = self._send_server_action_request("/servers/1/action", body)
self.assertEqual(501, response.status_int)
def test_invalid_action(self):
- app = openstack.APIRouter()
- ext_midware = extensions.ExtensionMiddleware(app)
- request = webob.Request.blank("/asdf/1/action")
- request.method = 'POST'
- request.content_type = 'application/json'
- response = request.get_response(ext_midware)
+ body = dict(blah=dict(name="test"))
+ response = self._send_server_action_request("/asdf/1/action", body)
self.assertEqual(404, response.status_int)
+
+
+class ResponseExtensionTest(unittest.TestCase):
+
+ def setUp(self):
+ super(ResponseExtensionTest, self).setUp()
+ self.stubs = stubout.StubOutForTesting()
+ fakes.FakeAuthManager.reset_fake_data()
+ fakes.FakeAuthDatabase.data = {}
+ fakes.stub_out_networking(self.stubs)
+ fakes.stub_out_rate_limiting(self.stubs)
+ fakes.stub_out_auth(self.stubs)
+ self.context = context.get_admin_context()
+
+ def test_get_resources(self):
+
+ test_resp = "Buy more widgets!"
+
+ def _resp_handler(res):
+ # only handle JSON responses
+ data = json.loads(res.body)
+ data['flavor']['widgets'] = test_resp
+ return data
+
+ widgets = extensions.ResponseExtension('/v1.0/flavors/:(id)', 'GET',
+ _resp_handler)
+ manager = StubExtensionManager(None, None, widgets)
+ app = fakes.wsgi_app()
+ ext_midware = extensions.ExtensionMiddleware(app, manager)
+ request = webob.Request.blank("/v1.0/flavors/1")
+ response = request.get_response(ext_midware)
+ self.assertEqual(200, response.status_int)
+ response_data = json.loads(response.body)
+ self.assertEqual(test_resp, response_data['flavor']['widgets'])