summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortermie <github@anarkystic.com>2011-06-20 18:37:51 -0700
committertermie <github@anarkystic.com>2011-06-20 18:37:51 -0700
commit158dfbac2aa91a8c0287f4398e11bc90f7d84ac0 (patch)
tree9ecafa0402ca3479aa9809844e8f5e3f887c4dcd
parent419c2cb95f5ce0c515fd8636d90065dfcf784c8c (diff)
most bits working
-rwxr-xr-xbin/keystone40
-rw-r--r--keystonelight/backends/pam.py2
-rw-r--r--keystonelight/identity.py4
-rw-r--r--keystonelight/service.py96
-rw-r--r--keystonelight/token.py14
-rw-r--r--keystonelight/utils.py7
-rw-r--r--keystonelight/wsgi.py358
-rw-r--r--tools/pip-requires5
8 files changed, 504 insertions, 22 deletions
diff --git a/bin/keystone b/bin/keystone
new file mode 100755
index 00000000..a3309f6d
--- /dev/null
+++ b/bin/keystone
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+import os
+import sys
+
+# If ../../keystone/__init__.py exists, add ../ to Python search path, so that
+# it will override what happens to be installed in /usr/(local/)lib/python...
+possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
+ os.pardir,
+ os.pardir))
+if os.path.exists(os.path.join(possible_topdir, 'keystonelight', '__init__.py')):
+ sys.path.insert(0, possible_topdir)
+
+import logging
+
+import hflags as flags
+
+from keystonelight import service
+from keystonelight import wsgi
+
+
+FLAGS = flags.FLAGS
+
+flags.DEFINE_boolean('verbose', True, 'verbose logging')
+
+if __name__ == '__main__':
+ args = FLAGS(sys.argv)
+ if FLAGS.verbose:
+ logging.getLogger().setLevel(logging.DEBUG)
+
+ public = service.Router()
+ admin = service.AdminRouter()
+
+ public = service.PostParamsMiddleware(public)
+
+ server = wsgi.Server()
+ server.start(public, 8080)
+ server.start(admin, 8081)
+ server.wait()
diff --git a/keystonelight/backends/pam.py b/keystonelight/backends/pam.py
index 8896c930..cfac3ed5 100644
--- a/keystonelight/backends/pam.py
+++ b/keystonelight/backends/pam.py
@@ -1,6 +1,6 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
-from __future__ import absolute_imports
+from __future__ import absolute_import
import pam
diff --git a/keystonelight/identity.py b/keystonelight/identity.py
index ee2345aa..8d6c7b6e 100644
--- a/keystonelight/identity.py
+++ b/keystonelight/identity.py
@@ -11,11 +11,11 @@ from keystonelight import utils
FLAGS = flags.FLAGS
flags.DEFINE_string('identity_driver',
- 'keystonelight.backends.dummy.DummyIdentity',
+ 'keystonelight.backends.pam.PamIdentity',
'identity driver to handle identity requests')
-class IdentityManager(object):
+class Manager(object):
def __init__(self):
self.driver = utils.import_object(FLAGS.identity_driver)
diff --git a/keystonelight/service.py b/keystonelight/service.py
index e436cebc..a06f99cb 100644
--- a/keystonelight/service.py
+++ b/keystonelight/service.py
@@ -2,26 +2,92 @@
# this is the web service frontend
+import json
+import logging
+
import hflags as flags
+import routes
+import webob.dec
+from keystonelight import identity
+from keystonelight import token
+from keystonelight import utils
from keystonelight import wsgi
FLAGS = flags.FLAGS
+# TODO(termie): these should probably be paste configs instead
+flags.DEFINE_string('token_controller',
+ 'keystonelight.service.TokenController',
+ 'token controller')
+flags.DEFINE_string('identity_controller',
+ 'keystonelight.service.IdentityController',
+ 'identity controller')
+
+
+class BaseApplication(wsgi.Application):
+ @webob.dec.wsgify
+ def __call__(self, req):
+ arg_dict = req.environ['wsgiorg.routing_args'][1]
+ action = arg_dict['action']
+ del arg_dict['action']
+ del arg_dict['controller']
+ logging.info('arg_dict: %s', arg_dict)
+
+ context = req.environ.get('openstack.context', {})
+ # allow middleware up the stack to override the params
+ params = {}
+ if 'openstack.params' in req.environ:
+ params = req.environ['openstack.params']
+ params.update(arg_dict)
+
+ # TODO(termie): do some basic normalization on methods
+ method = getattr(self, action)
+
+ # NOTE(vish): make sure we have no unicode keys for py2.6.
+ params = dict([(str(k), v) for (k, v) in params.iteritems()])
+ result = method(context, **params)
+
+ if result is None or type(result) is str or type(result) is unicode:
+ return result
+
+ return json.dumps(result)
+
-class TokenController(wsgi.Controller):
+class PostParamsMiddleware(wsgi.Middleware):
+ """Middleware to allow method arguments to be passed as POST parameters.
+
+ Filters out the parameters `self`, `context` and anything beginning with
+ an underscore.
+
+ """
+
+ 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 TokenController(BaseApplication):
"""Validate and pass through calls to TokenManager."""
def __init__(self):
self.token_api = token.Manager()
def validate_token(self, context, token_id):
- token = self.validate_token(context, token_id)
+ token = self.token_api.validate_token(context, token_id)
return token
-class IdentityController(wsgi.Controller):
+class IdentityController(BaseApplication):
"""Validate and pass calls through to IdentityManager.
IdentityManager will also pretty much just pass calls through to
@@ -35,29 +101,33 @@ class IdentityController(wsgi.Controller):
def authenticate(self, context, **kwargs):
tenant, user, extras = self.identity_api.authenticate(context, **kwargs)
token = self.token_api.create_token(context,
- tenant=tenant,
- user=user,
- extras=extras)
+ dict(tenant=tenant,
+ user=user,
+ extras=extras))
+ logging.info(token)
return token
-class Router(object):
+class Router(wsgi.Router):
def __init__(self):
- token_controller = TokenController()
- identity_controller = IdentityController()
+ token_controller = utils.import_object(FLAGS.token_controller)
+ identity_controller = utils.import_object(FLAGS.identity_controller)
+ mapper = routes.Mapper()
mapper.connect('/v2.0/token', controller=identity_controller,
action='authenticate')
mapper.connect('/v2.0/token/{token_id}', controller=token_controller,
action='revoke_token',
conditions=dict(method=['DELETE']))
+ super(Router, self).__init__(mapper)
-class AdminRouter(object):
+class AdminRouter(wsgi.Router):
def __init__(self):
- token_controller = TokenController()
- identity_controller = IdentityController()
+ token_controller = utils.import_object(FLAGS.token_controller)
+ identity_controller = utils.import_object(FLAGS.identity_controller)
+ mapper = routes.Mapper()
mapper.connect('/v2.0/token', controller=identity_controller,
action='authenticate')
@@ -67,4 +137,4 @@ class AdminRouter(object):
mapper.connect('/v2.0/token/{token_id}', controller=token_controller,
action='revoke_token',
conditions=dict(method=['DELETE']))
-
+ super(AdminRouter, self).__init__(mapper)
diff --git a/keystonelight/token.py b/keystonelight/token.py
index ffdd4c4c..5221fe9e 100644
--- a/keystonelight/token.py
+++ b/keystonelight/token.py
@@ -2,15 +2,21 @@
# the token interfaces
+import uuid
+
from keystonelight import identity
-class TokenManager(object):
+STORE = {}
+
+class Manager(object):
def create_token(self, context, data):
- pass
+ token = uuid.uuid4().hex
+ STORE[token] = data
+ return token
def validate_token(self, context, token_id):
"""Return info for a token if it is valid."""
- pass
+ return STORE.get(token_id)
def revoke_token(self, context, token_id):
- pass
+ STORE.pop(token_id)
diff --git a/keystonelight/utils.py b/keystonelight/utils.py
index 406ad19e..96f9185e 100644
--- a/keystonelight/utils.py
+++ b/keystonelight/utils.py
@@ -17,6 +17,9 @@
# License for the specific language governing permissions and limitations
# under the License.
+import logging
+import sys
+
def import_class(import_str):
"""Returns a class from a string including module and class."""
@@ -25,8 +28,8 @@ def import_class(import_str):
__import__(mod_str)
return getattr(sys.modules[mod_str], class_str)
except (ImportError, ValueError, AttributeError), exc:
- LOG.debug(_('Inner Exception: %s'), exc)
- raise exception.ClassNotFound(class_name=class_str)
+ logging.debug('Inner Exception: %s', exc)
+ raise
def import_object(import_str):
diff --git a/keystonelight/wsgi.py b/keystonelight/wsgi.py
new file mode 100644
index 00000000..5eea4908
--- /dev/null
+++ b/keystonelight/wsgi.py
@@ -0,0 +1,358 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# Copyright 2010 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.
+
+"""Utility methods for working with WSGI servers."""
+
+import logging
+import os
+import sys
+
+import eventlet
+import eventlet.wsgi
+eventlet.patcher.monkey_patch(all=False, socket=True, time=True)
+import hflags as flags
+import routes
+import routes.middleware
+import webob
+import webob.dec
+import webob.exc
+from paste import deploy
+
+
+FLAGS = flags.FLAGS
+
+
+class WritableLogger(object):
+ """A thin wrapper that responds to `write` and logs."""
+
+ def __init__(self, logger, level=logging.DEBUG):
+ self.logger = logger
+ self.level = level
+
+ def write(self, msg):
+ self.logger.log(self.level, msg)
+
+
+class Server(object):
+ """Server class to manage multiple WSGI sockets and applications."""
+
+ def __init__(self, threads=1000):
+ self.pool = eventlet.GreenPool(threads)
+ self.socket_info = {}
+
+ def start(self, application, port, host='0.0.0.0', key=None, backlog=128):
+ """Run a WSGI server with the given application."""
+ arg0 = sys.argv[0]
+ logging.debug('Starting %(arg0)s on %(host)s:%(port)s' % locals())
+ socket = eventlet.listen((host, port), backlog=backlog)
+ self.pool.spawn_n(self._run, application, socket)
+ if key:
+ self.socket_info[key] = socket.getsockname()
+
+ def wait(self):
+ """Wait until all servers have completed running."""
+ try:
+ self.pool.waitall()
+ except KeyboardInterrupt:
+ pass
+
+ def _run(self, application, socket):
+ """Start a WSGI server in a new green thread."""
+ logger = logging.getLogger('eventlet.wsgi.server')
+ eventlet.wsgi.server(socket, application, custom_pool=self.pool,
+ log=WritableLogger(logger))
+
+
+class Request(webob.Request):
+ pass
+
+
+class Application(object):
+ """Base WSGI application wrapper. Subclasses need to implement __call__."""
+
+ @classmethod
+ def factory(cls, global_config, **local_config):
+ """Used for paste app factories in paste.deploy config files.
+
+ Any local configuration (that is, values under the [app:APPNAME]
+ section of the paste config) will be passed into the `__init__` method
+ as kwargs.
+
+ A hypothetical configuration would look like:
+
+ [app:wadl]
+ latest_version = 1.3
+ paste.app_factory = nova.api.fancy_api:Wadl.factory
+
+ which would result in a call to the `Wadl` class as
+
+ import nova.api.fancy_api
+ fancy_api.Wadl(latest_version='1.3')
+
+ You could of course re-implement the `factory` method in subclasses,
+ but using the kwarg passing it shouldn't be necessary.
+
+ """
+ return cls(**local_config)
+
+ def __call__(self, environ, start_response):
+ r"""Subclasses will probably want to implement __call__ like this:
+
+ @webob.dec.wsgify(RequestClass=Request)
+ def __call__(self, req):
+ # Any of the following objects work as responses:
+
+ # Option 1: simple string
+ res = 'message\n'
+
+ # Option 2: a nicely formatted HTTP exception page
+ res = exc.HTTPForbidden(detail='Nice try')
+
+ # Option 3: a webob Response object (in case you need to play with
+ # headers, or you want to be treated like an iterable, or or or)
+ res = Response();
+ res.app_iter = open('somefile')
+
+ # Option 4: any wsgi app to be run next
+ res = self.application
+
+ # Option 5: you can get a Response object for a wsgi app, too, to
+ # play with headers etc
+ res = req.get_response(self.application)
+
+ # You can then just return your response...
+ return res
+ # ... or set req.response and return None.
+ req.response = res
+
+ See the end of http://pythonpaste.org/webob/modules/dec.html
+ for more info.
+
+ """
+ raise NotImplementedError('You must implement __call__')
+
+
+class Middleware(Application):
+ """Base WSGI middleware.
+
+ 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.
+
+ """
+
+ @classmethod
+ def factory(cls, global_config, **local_config):
+ """Used for paste app factories in paste.deploy config files.
+
+ Any local configuration (that is, values under the [filter:APPNAME]
+ section of the paste config) will be passed into the `__init__` method
+ as kwargs.
+
+ A hypothetical configuration would look like:
+
+ [filter:analytics]
+ redis_host = 127.0.0.1
+ paste.filter_factory = nova.api.analytics:Analytics.factory
+
+ which would result in a call to the `Analytics` class as
+
+ import nova.api.analytics
+ analytics.Analytics(app_from_paste, redis_host='127.0.0.1')
+
+ You could of course re-implement the `factory` method in subclasses,
+ but using the kwarg passing it shouldn't be necessary.
+
+ """
+ def _factory(app):
+ return cls(app, **local_config)
+ return _factory
+
+ 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(RequestClass=Request)
+ 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):
+ """Helper class for debugging a WSGI application.
+
+ Can be inserted into any WSGI application chain to get information
+ about the request and response.
+
+ """
+
+ @webob.dec.wsgify(RequestClass=Request)
+ def __call__(self, req):
+ print ('*' * 40) + ' REQUEST ENVIRON'
+ for key, value in req.environ.items():
+ print key, '=', value
+ print
+ resp = req.get_response(self.application)
+
+ print ('*' * 40) + ' RESPONSE HEADERS'
+ for (key, value) in resp.headers.iteritems():
+ print key, '=', value
+ print
+
+ resp.app_iter = self.print_generator(resp.app_iter)
+
+ return resp
+
+ @staticmethod
+ def print_generator(app_iter):
+ """Iterator that prints the contents of a wrapper string."""
+ print ('*' * 40) + ' BODY'
+ for part in app_iter:
+ sys.stdout.write(part)
+ sys.stdout.flush()
+ yield part
+ print
+
+
+class Router(object):
+ """WSGI middleware that maps incoming requests to WSGI apps."""
+
+ def __init__(self, mapper):
+ """Create a router for the given routes.Mapper.
+
+ Each route in `mapper` must specify a 'controller', which is a
+ WSGI app to call. You'll probably want to specify an 'action' as
+ well and have your controller be an object that can route
+ the request to the action-specific method.
+
+ Examples:
+ mapper = routes.Mapper()
+ sc = ServerController()
+
+ # Explicit mapping of one route to a controller+action
+ mapper.connect(None, '/svrlist', controller=sc, action='list')
+
+ # Actions are all implicitly defined
+ mapper.resource('server', 'servers', controller=sc)
+
+ # Pointing to an arbitrary WSGI app. You can specify the
+ # {path_info:.*} parameter so the target app can be handed just that
+ # section of the URL.
+ mapper.connect(None, '/v1.0/{path_info:.*}', controller=BlogApp())
+
+ """
+ self.map = mapper
+ self._router = routes.middleware.RoutesMiddleware(self._dispatch,
+ self.map)
+
+ @webob.dec.wsgify(RequestClass=Request)
+ def __call__(self, req):
+ """Route the incoming request to a controller based on self.map.
+
+ If no match, return a 404.
+
+ """
+ return self._router
+
+ @staticmethod
+ @webob.dec.wsgify(RequestClass=Request)
+ def _dispatch(req):
+ """Dispatch the request to the appropriate controller.
+
+ Called by self._router after matching the incoming request to a route
+ and putting the information into req.environ. Either returns 404
+ or the routed WSGI app's response.
+
+ """
+ match = req.environ['wsgiorg.routing_args'][1]
+ if not match:
+ return webob.exc.HTTPNotFound()
+ app = match['controller']
+ return app
+
+
+def paste_config_file(basename):
+ """Find the best location in the system for a paste config file.
+
+ Search Order
+ ------------
+
+ The search for a paste config file honors `FLAGS.state_path`, which in a
+ version checked out from bzr will be the `nova` directory in the top level
+ of the checkout, and in an installation for a package for your distribution
+ will likely point to someplace like /etc/nova.
+
+ This method tries to load places likely to be used in development or
+ experimentation before falling back to the system-wide configuration
+ in `/etc/nova/`.
+
+ * Current working directory
+ * the `etc` directory under state_path, because when working on a checkout
+ from bzr this will point to the default
+ * top level of FLAGS.state_path, for distributions
+ * /etc/nova, which may not be diffrerent from state_path on your distro
+
+ """
+ configfiles = [basename,
+ os.path.join(FLAGS.state_path, 'etc', 'nova', basename),
+ os.path.join(FLAGS.state_path, 'etc', basename),
+ os.path.join(FLAGS.state_path, basename),
+ '/etc/nova/%s' % basename]
+ for configfile in configfiles:
+ if os.path.exists(configfile):
+ return configfile
+
+
+def load_paste_configuration(filename, appname):
+ """Returns a paste configuration dict, or None."""
+ filename = os.path.abspath(filename)
+ config = None
+ try:
+ config = deploy.appconfig('config:%s' % filename, name=appname)
+ except LookupError:
+ pass
+ return config
+
+
+def load_paste_app(filename, appname):
+ """Builds a wsgi app from a paste config, None if app not configured."""
+ filename = os.path.abspath(filename)
+ app = None
+ try:
+ app = deploy.loadapp('config:%s' % filename, name=appname)
+ except LookupError:
+ pass
+ return app
diff --git a/tools/pip-requires b/tools/pip-requires
index 14d62412..30a8d583 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -1,2 +1,7 @@
hflags
pam==0.1.4
+WebOb==0.9.8
+eventlet==0.9.12
+PasteDeploy
+paste
+routes