diff options
| author | termie <github@anarkystic.com> | 2011-06-20 18:37:51 -0700 |
|---|---|---|
| committer | termie <github@anarkystic.com> | 2011-06-20 18:37:51 -0700 |
| commit | 158dfbac2aa91a8c0287f4398e11bc90f7d84ac0 (patch) | |
| tree | 9ecafa0402ca3479aa9809844e8f5e3f887c4dcd | |
| parent | 419c2cb95f5ce0c515fd8636d90065dfcf784c8c (diff) | |
most bits working
| -rwxr-xr-x | bin/keystone | 40 | ||||
| -rw-r--r-- | keystonelight/backends/pam.py | 2 | ||||
| -rw-r--r-- | keystonelight/identity.py | 4 | ||||
| -rw-r--r-- | keystonelight/service.py | 96 | ||||
| -rw-r--r-- | keystonelight/token.py | 14 | ||||
| -rw-r--r-- | keystonelight/utils.py | 7 | ||||
| -rw-r--r-- | keystonelight/wsgi.py | 358 | ||||
| -rw-r--r-- | tools/pip-requires | 5 |
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 |
