# Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # Copyright 2010 OpenStack Foundation # 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.""" from __future__ import print_function import copy import os import socket import eventlet import eventlet.wsgi import greenlet from paste import deploy import routes.middleware import webob.dec import webob.exc from oslo_log import log as logging from oslo_service import _options from oslo_service import service from oslo_service import sslutils from oslo_service._i18n import _, _LE, _LI LOG = logging.getLogger(__name__) def list_opts(): """Entry point for oslo-config-generator.""" return [(None, copy.deepcopy(_options.wsgi_opts))] def register_opts(conf): """Registers WSGI config options.""" return conf.register_opts(_options.wsgi_opts) class InvalidInput(Exception): message = _("Invalid input received: " "Unexpected argument for periodic task creation: %(arg)s.") class Server(service.ServiceBase): """Server class to manage a WSGI server, serving a WSGI application.""" def __init__(self, conf, name, app, host='0.0.0.0', port=0, pool_size=None, protocol=eventlet.wsgi.HttpProtocol, backlog=128, use_ssl=False, max_url_len=None): """Initialize, but do not start, a WSGI server. :param conf: Instance of ConfigOpts. :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. :param protocol: Protocol class. :param backlog: Maximum number of queued connections. :param use_ssl: Wraps the socket in an SSL context if True. :param max_url_len: Maximum length of permitted URLs. :returns: None :raises: InvalidInput :raises: EnvironmentError """ self.conf = conf self.conf.register_opts(_options.wsgi_opts) self.default_pool_size = self.conf.wsgi_default_pool_size # Allow operators to customize http requests max header line size. eventlet.wsgi.MAX_HEADER_LINE = conf.max_header_line self.name = name self.app = app self._server = None self._protocol = protocol self.pool_size = pool_size or self.default_pool_size self._pool = eventlet.GreenPool(self.pool_size) self._logger = logging.getLogger("eventlet.wsgi.server") self._use_ssl = use_ssl self._max_url_len = max_url_len self.client_socket_timeout = conf.client_socket_timeout or None if backlog < 1: raise InvalidInput(reason=_('The backlog must be more than 0')) bind_addr = (host, port) # TODO(dims): eventlet's green dns/socket module does not actually # support IPv6 in getaddrinfo(). We need to get around this in the # future or monitor upstream for a fix try: info = socket.getaddrinfo(bind_addr[0], bind_addr[1], socket.AF_UNSPEC, socket.SOCK_STREAM)[0] family = info[0] bind_addr = info[-1] except Exception: family = socket.AF_INET if self._use_ssl: sslutils.is_enabled(conf) try: self._socket = eventlet.listen(bind_addr, family, backlog=backlog) except EnvironmentError: LOG.error(_LE("Could not bind to %(host)s:%(port)s"), {'host': host, 'port': port}) raise (self.host, self.port) = self._socket.getsockname()[0:2] LOG.info(_LI("%(name)s listening on %(host)s:%(port)s"), {'name': self.name, 'host': self.host, 'port': self.port}) def start(self): """Start serving a WSGI application. :returns: None """ # The server socket object will be closed after server exits, # but the underlying file descriptor will remain open, and will # give bad file descriptor error. So duplicating the socket object, # to keep file descriptor usable. self.dup_socket = self._socket.dup() self.dup_socket = self._set_socket_opts(self.dup_socket) if self._use_ssl: self.dup_socket = sslutils.wrap(self.conf, self.dup_socket) wsgi_kwargs = { 'func': eventlet.wsgi.server, 'sock': self.dup_socket, 'site': self.app, 'protocol': self._protocol, 'custom_pool': self._pool, 'log': self._logger, 'log_format': self.conf.wsgi_log_format, 'debug': False, 'keepalive': self.conf.wsgi_keep_alive, 'socket_timeout': self.client_socket_timeout } if self._max_url_len: wsgi_kwargs['url_length_limit'] = self._max_url_len self._server = eventlet.spawn(**wsgi_kwargs) def _set_socket_opts(self, _socket): _socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # sockets can hang around forever without keepalive _socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) # This option isn't available in the OS X version of eventlet if hasattr(socket, 'TCP_KEEPIDLE'): _socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, self.conf.tcp_keepidle) return _socket def reset(self): """Reset server greenpool size to default. :returns: None """ self._pool.resize(self.pool_size) def stop(self): """Stops eventlet server. Doesn't allow accept new connecting. :returns: None """ LOG.info(_LI("Stopping WSGI server.")) if self._server is not None: # let eventlet close socket self._pool.resize(0) self._server.kill() def wait(self): """Block, until the server has stopped. Waits on the server's eventlet to finish, then returns. :returns: None """ try: if self._server is not None: num = self._pool.running() LOG.debug("Waiting WSGI server to finish %d requests.", num) self._pool.waitall() except greenlet.GreenletExit: LOG.info(_LI("WSGI server has stopped.")) class Request(webob.Request): pass 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 ConfigNotFound(Exception): def __init__(self, path): msg = _('Could not find config at %(path)s') % {'path': path} super(ConfigNotFound, self).__init__(msg) class PasteAppNotFound(Exception): def __init__(self, name, path): msg = (_("Could not load paste app '%(name)s' from %(path)s") % {'name': name, 'path': path}) super(PasteAppNotFound, self).__init__(msg) class Loader(object): """Used to load WSGI applications from paste configurations.""" def __init__(self, conf): """Initialize the loader, and attempt to find the config. :param conf :returns: None """ conf.register_opts(_options.wsgi_opts) self.config_path = None config_path = conf.api_paste_config if not os.path.isabs(config_path): self.config_path = conf.find_file(config_path) elif os.path.exists(config_path): self.config_path = config_path if not self.config_path: raise ConfigNotFound(path=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: `PasteAppNotFound` """ try: LOG.debug("Loading app %(name)s from %(path)s", {'name': name, 'path': self.config_path}) return deploy.loadapp("config:%s" % self.config_path, name=name) except LookupError: LOG.exception(_LE("Couldn't lookup app: %s"), name) raise PasteAppNotFound(name=name, path=self.config_path)