summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndy Smith <code@term.ie>2010-12-22 16:43:47 -0800
committerAndy Smith <code@term.ie>2010-12-22 16:43:47 -0800
commit4ff2da231d485598232d9aacc41538950005ac34 (patch)
tree1c1897d3a3057be024d9257dda3889d12c447f81
parent5cae69f5e5a3f8cfb33de0564940b3f45498d7dc (diff)
Basic Easy API functionality
-rw-r--r--nova/api/easy.py163
-rw-r--r--nova/tests/easy_unittest.py85
-rw-r--r--nova/wsgi.py31
-rw-r--r--run_tests.py1
4 files changed, 274 insertions, 6 deletions
diff --git a/nova/api/easy.py b/nova/api/easy.py
new file mode 100644
index 000000000..a284e9685
--- /dev/null
+++ b/nova/api/easy.py
@@ -0,0 +1,163 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# 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.
+
+"""Public HTTP interface that allows services to self-register.
+
+The general flow of a request is:
+ - Request is parsed into WSGI bits.
+ - Some middleware checks authentication.
+ - Routing takes place based on the URL to find a controller.
+ (/controller/method)
+ - Parameters are parsed from the request and passed to a method on the
+ controller as keyword arguments.
+ - Optionally json_body is decoded to provide all the parameters.
+ - Actual work is done and a result is returned.
+ - That result is turned into json and returned.
+
+"""
+
+import json
+import urllib
+
+import routes
+import webob
+
+from nova import context
+from nova import flags
+from nova import wsgi
+
+# prxy compute_api in amazon tests
+
+
+EASY_ROUTES = {}
+
+
+def register_service(path, handle):
+ EASY_ROUTES[path] = handle
+
+
+class DelegatedAuthMiddleware(wsgi.Middleware):
+ def process_request(self, request):
+ os_user = request.headers['X-OpenStack-User']
+ os_project = request.headers['X-OpenStack-Project']
+ context_ref = context.RequestContext(user=os_user, project=os_project)
+ request.environ['openstack.context'] = context_ref
+
+
+class JsonParamsMiddleware(wsgi.Middleware):
+ def process_request(self, request):
+ if 'json' not in request.params:
+ return
+
+ params_json = request.params['json']
+ params_parsed = json.loads(params_json)
+ params = {}
+ for k, v in params_parsed.iteritems():
+ if k in ('self', 'context'):
+ continue
+ if k.startswith('_'):
+ continue
+ params[k] = v
+
+ request.environ['openstack.params'] = params
+
+
+class ReqParamsMiddleware(wsgi.Middleware):
+ def process_request(self, request):
+ params_parsed = request.params
+ params = {}
+ for k, v in params_parsed.iteritems():
+ if k in ('self', 'context'):
+ continue
+ if k.startswith('_'):
+ continue
+ params[k] = v
+
+ request.environ['openstack.params'] = params
+
+
+class SundayMorning(wsgi.Router):
+ def __init__(self, mapper=None):
+ if mapper is None:
+ mapper = routes.Mapper()
+
+ self._load_registered_routes(mapper)
+ super(SundayMorning, self).__init__(mapper=mapper)
+
+ def _load_registered_routes(self, mapper):
+ for route in EASY_ROUTES:
+ mapper.connect('/%s/{action}' % route,
+ controller=ServiceWrapper(EASY_ROUTES[route]))
+
+
+class ServiceWrapper(wsgi.Controller):
+ def __init__(self, service_handle):
+ self.service_handle = service_handle
+
+ @webob.dec.wsgify
+ def __call__(self, req):
+ arg_dict = req.environ['wsgiorg.routing_args'][1]
+ action = arg_dict['action']
+ del arg_dict['action']
+
+ context = req.environ['openstack.context']
+ # allow middleware up the stack to override the params
+ params = {}
+ if 'openstack.params' in req.environ:
+ params = req.environ['openstack.params']
+
+ # TODO(termie): do some basic normalization on methods
+ method = getattr(self.service_handle, action)
+
+ result = method(context, **params)
+ if type(result) is dict:
+ return self._serialize(result, req)
+ else:
+ return result
+
+
+class Proxy(object):
+ """Pretend an Easy API endpoint is an object."""
+ def __init__(self, app, prefix=None):
+ self.app = app
+ self.prefix = prefix
+
+ def __do_request(self, path, context, **kwargs):
+ req = webob.Request.blank(path)
+ req.method = 'POST'
+ req.body = urllib.urlencode({'json': json.dumps(kwargs)})
+ req.environ['openstack.context'] = context
+ resp = req.get_response(self.app)
+ try:
+ return json.loads(resp.body)
+ except Exception:
+ return resp.body
+
+ def __getattr__(self, key):
+ if self.prefix is None:
+ return self.__class__(self.app, key)
+
+ def _wrapper(context, **kwargs):
+ return self.__do_request('/%s/%s' % (self.prefix, key),
+ context,
+ **kwargs)
+ _wrapper.func_name = key
+ return _wrapper
+
+
+
diff --git a/nova/tests/easy_unittest.py b/nova/tests/easy_unittest.py
new file mode 100644
index 000000000..ed223831f
--- /dev/null
+++ b/nova/tests/easy_unittest.py
@@ -0,0 +1,85 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# 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.
+
+"""Tests for Easy API."""
+
+import json
+import logging
+
+import webob
+
+from nova import context
+from nova import exception
+from nova import test
+from nova import utils
+from nova.api import easy
+
+
+class FakeService(object):
+ def echo(self, context, data):
+ return {'data': data}
+
+ def context(self, context):
+ return {'user': context.user_id,
+ 'project': context.project_id}
+
+
+class EasyTestCase(test.TestCase):
+ def setUp(self):
+ super(EasyTestCase, self).setUp()
+ easy.register_service('fake', FakeService())
+ self.router = easy.ReqParamsMiddleware(
+ easy.JsonParamsMiddleware(
+ easy.SundayMorning()))
+ self.auth_router = easy.DelegatedAuthMiddleware(self.router)
+ self.context = context.RequestContext('user1', 'proj1')
+
+ def tearDown(self):
+ easy.EASY_ROUTES = {}
+
+ def test_delegated_auth(self):
+ req = webob.Request.blank('/fake/context')
+ req.headers['X-OpenStack-User'] = 'user1'
+ req.headers['X-OpenStack-Project'] = 'proj1'
+ resp = req.get_response(self.auth_router)
+ data = json.loads(resp.body)
+ self.assertEqual(data['user'], 'user1')
+ self.assertEqual(data['project'], 'proj1')
+
+ def test_json_params(self):
+ req = webob.Request.blank('/fake/echo')
+ req.environ['openstack.context'] = self.context
+ req.method = 'POST'
+ req.body = 'json=%s' % json.dumps({'data': 'foo'})
+ resp = req.get_response(self.router)
+ resp_parsed = json.loads(resp.body)
+ self.assertEqual(resp_parsed['data'], 'foo')
+
+ def test_req_params(self):
+ req = webob.Request.blank('/fake/echo')
+ req.environ['openstack.context'] = self.context
+ req.method = 'POST'
+ req.body = 'data=foo'
+ resp = req.get_response(self.router)
+ resp_parsed = json.loads(resp.body)
+ self.assertEqual(resp_parsed['data'], 'foo')
+
+ def test_proxy(self):
+ proxy = easy.Proxy(self.router)
+ rv = proxy.fake.echo(self.context, data='baz')
+ self.assertEqual(rv['data'], 'baz')
diff --git a/nova/wsgi.py b/nova/wsgi.py
index c7ee9ed14..2ad27dd7e 100644
--- a/nova/wsgi.py
+++ b/nova/wsgi.py
@@ -104,20 +104,39 @@ class Application(object):
class Middleware(Application):
- """
- Base WSGI middleware wrapper. These classes require an application to be
+ """Base WSGI middleware.
+
+ Modelled after Django's middleware this class allows you to
+ These classes require an application to be
initialized that will be called next. By default the middleware will
simply call its wrapped app, or you can override __call__ to customize its
behavior.
"""
- def __init__(self, application): # pylint: disable-msg=W0231
+ def __init__(self, application):
self.application = application
+ def process_request(self, req):
+ """Called on each request.
+
+ If this returns None, the next application down the stack will be
+ executed. If it returns a response then that response will be returned
+ and execution will stop here.
+
+ """
+ return None
+
+ def process_response(self, response):
+ """Do whatever you'd like to the response."""
+ return response
+
@webob.dec.wsgify
- def __call__(self, req): # pylint: disable-msg=W0221
- """Override to implement middleware behavior."""
- return self.application
+ def __call__(self, req):
+ response = self.process_request(req)
+ if response:
+ return response
+ response = req.get_response(self.application)
+ return self.process_response(response)
class Debug(Middleware):
diff --git a/run_tests.py b/run_tests.py
index 6a4b7f1ab..d3cf8f696 100644
--- a/run_tests.py
+++ b/run_tests.py
@@ -59,6 +59,7 @@ from nova.tests.api_unittest import *
from nova.tests.auth_unittest import *
from nova.tests.cloud_unittest import *
from nova.tests.compute_unittest import *
+from nova.tests.easy_unittest import *
from nova.tests.flags_unittest import *
from nova.tests.misc_unittest import *
from nova.tests.network_unittest import *