389 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			389 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # 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 sys
 | |
| 
 | |
| import eventlet
 | |
| import eventlet.wsgi
 | |
| import greenlet
 | |
| from paste import deploy
 | |
| import routes.middleware
 | |
| import webob.dec
 | |
| import webob.exc
 | |
| 
 | |
| from nova import exception
 | |
| from nova import flags
 | |
| from nova import log as logging
 | |
| from nova import utils
 | |
| 
 | |
| 
 | |
| FLAGS = flags.FLAGS
 | |
| LOG = logging.getLogger('nova.wsgi')
 | |
| 
 | |
| 
 | |
| class Server(object):
 | |
|     """Server class to manage a WSGI server, serving a WSGI application."""
 | |
| 
 | |
|     default_pool_size = 1000
 | |
| 
 | |
|     def __init__(self, name, app, host=None, port=None, pool_size=None):
 | |
|         """Initialize, but do not start, a WSGI server.
 | |
| 
 | |
|         :param name: Pretty name for logging.
 | |
|         :param app: The WSGI application to serve.
 | |
|         :param host: IP address to serve the application.
 | |
|         :param port: Port number to server the application.
 | |
|         :param pool_size: Maximum number of eventlets to spawn concurrently.
 | |
|         :returns: None
 | |
| 
 | |
|         """
 | |
|         self.name = name
 | |
|         self.app = app
 | |
|         self.host = host or "0.0.0.0"
 | |
|         self.port = port or 0
 | |
|         self._server = None
 | |
|         self._tcp_server = None
 | |
|         self._socket = None
 | |
|         self._pool = eventlet.GreenPool(pool_size or self.default_pool_size)
 | |
|         self._logger = logging.getLogger("eventlet.wsgi.server")
 | |
|         self._wsgi_logger = logging.WritableLogger(self._logger)
 | |
| 
 | |
|     def _start(self):
 | |
|         """Run the blocking eventlet WSGI server.
 | |
| 
 | |
|         :returns: None
 | |
| 
 | |
|         """
 | |
|         eventlet.wsgi.server(self._socket,
 | |
|                              self.app,
 | |
|                              custom_pool=self._pool,
 | |
|                              log=self._wsgi_logger)
 | |
| 
 | |
|     def start(self, backlog=128):
 | |
|         """Start serving a WSGI application.
 | |
| 
 | |
|         :param backlog: Maximum number of queued connections.
 | |
|         :returns: None
 | |
| 
 | |
|         """
 | |
|         self._socket = eventlet.listen((self.host, self.port), backlog=backlog)
 | |
|         self._server = eventlet.spawn(self._start)
 | |
|         (self.host, self.port) = self._socket.getsockname()
 | |
|         LOG.info(_("Started %(name)s on %(host)s:%(port)s") % self.__dict__)
 | |
| 
 | |
|     def stop(self):
 | |
|         """Stop this server.
 | |
| 
 | |
|         This is not a very nice action, as currently the method by which a
 | |
|         server is stopped is by killing it's eventlet.
 | |
| 
 | |
|         :returns: None
 | |
| 
 | |
|         """
 | |
|         LOG.info(_("Stopping WSGI server."))
 | |
|         self._server.kill()
 | |
|         if self._tcp_server is not None:
 | |
|             LOG.info(_("Stopping raw TCP server."))
 | |
|             self._tcp_server.kill()
 | |
| 
 | |
|     def start_tcp(self, listener, port, host='0.0.0.0', key=None, backlog=128):
 | |
|         """Run a raw TCP server with the given application."""
 | |
|         arg0 = sys.argv[0]
 | |
|         LOG.info(_('Starting TCP server %(arg0)s on %(host)s:%(port)s')
 | |
|                  % locals())
 | |
|         socket = eventlet.listen((host, port), backlog=backlog)
 | |
|         self._tcp_server = self._pool.spawn_n(self._run_tcp, listener, socket)
 | |
| 
 | |
|     def wait(self):
 | |
|         """Block, until the server has stopped.
 | |
| 
 | |
|         Waits on the server's eventlet to finish, then returns.
 | |
| 
 | |
|         :returns: None
 | |
| 
 | |
|         """
 | |
|         try:
 | |
|             self._server.wait()
 | |
|         except greenlet.GreenletExit:
 | |
|             LOG.info(_("WSGI server has stopped."))
 | |
| 
 | |
|     def _run_tcp(self, listener, socket):
 | |
|         """Start a raw TCP server in a new green thread."""
 | |
|         while True:
 | |
|             try:
 | |
|                 new_sock, address = socket.accept()
 | |
|                 self._pool.spawn_n(listener, new_sock)
 | |
|             except (SystemExit, KeyboardInterrupt):
 | |
|                 pass
 | |
| 
 | |
| 
 | |
| 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
 | |
| 
 | |
| 
 | |
| class Loader(object):
 | |
|     """Used to load WSGI applications from paste configurations."""
 | |
| 
 | |
|     def __init__(self, config_path=None):
 | |
|         """Initialize the loader, and attempt to find the config.
 | |
| 
 | |
|         :param config_path: Full or relative path to the paste config.
 | |
|         :returns: None
 | |
| 
 | |
|         """
 | |
|         config_path = config_path or FLAGS.api_paste_config
 | |
|         self.config_path = utils.find_config(config_path)
 | |
| 
 | |
|     def load_app(self, name):
 | |
|         """Return the paste URLMap wrapped WSGI application.
 | |
| 
 | |
|         :param name: Name of the application to load.
 | |
|         :returns: Paste URLMap object wrapping the requested application.
 | |
|         :raises: `nova.exception.PasteAppNotFound`
 | |
| 
 | |
|         """
 | |
|         try:
 | |
|             return deploy.loadapp("config:%s" % self.config_path, name=name)
 | |
|         except LookupError as err:
 | |
|             LOG.error(err)
 | |
|             raise exception.PasteAppNotFound(name=name, path=self.config_path)
 | 
