General cleanup and refactor of a lot of the API/WSGI service code.

This commit is contained in:
Brian Lamar
2011-06-18 23:10:41 -04:00
parent 21f5e441d7
commit 0d8cb633b7
5 changed files with 92 additions and 113 deletions

View File

@@ -24,6 +24,8 @@ import gettext
import os import os
import sys import sys
import eventlet.pool
# If ../nova/__init__.py exists, add ../ to Python search path, so that # If ../nova/__init__.py exists, add ../ to Python search path, so that
# it will override what happens to be installed in /usr/(local/)lib/python... # 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]), possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
@@ -46,6 +48,13 @@ LOG = logging.getLogger('nova.api')
FLAGS = flags.FLAGS FLAGS = flags.FLAGS
def launch(service_name):
_service = service.WSGIService(service_name)
_service.start()
_service.wait()
if __name__ == '__main__': if __name__ == '__main__':
utils.default_flagfile() utils.default_flagfile()
FLAGS(sys.argv) FLAGS(sys.argv)
@@ -57,5 +66,10 @@ if __name__ == '__main__':
flag_get = FLAGS.get(flag, None) flag_get = FLAGS.get(flag, None)
LOG.debug("%(flag)s : %(flag_get)s" % locals()) LOG.debug("%(flag)s : %(flag_get)s" % locals())
service = service.serve_wsgi(service.ApiService)
service.wait() pool = eventlet.pool.Pool()
pool.execute(launch, "ec2")
pool.execute(launch, "osapi")
pool.wait_all()
print >>sys.stderr, "Exiting..."

View File

@@ -314,3 +314,16 @@ logging.setLoggerClass(NovaLogger)
def audit(msg, *args, **kwargs): def audit(msg, *args, **kwargs):
"""Shortcut for logging to root log with sevrity 'AUDIT'.""" """Shortcut for logging to root log with sevrity 'AUDIT'."""
logging.root.log(AUDIT, msg, *args, **kwargs) logging.root.log(AUDIT, msg, *args, **kwargs)
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)

View File

@@ -232,45 +232,45 @@ class Service(object):
logging.exception(_('model server went away')) logging.exception(_('model server went away'))
class WsgiService(object): class WSGIService(object):
"""Base class for WSGI based services. """Provides ability to launch API from a 'paste' configuration."""
For each api you define, you must also define these flags: def __init__(self, name, config_name=None):
:<api>_listen: The address on which to listen """Initialize, but do not start, an API service."""
:<api>_listen_port: The port on which to listen self.name = name
self._config_name = config_name or FLAGS.api_paste_config
self._config_location = self._find_config()
self._config = self._load_config()
self.application = self._load_application()
host = getattr(FLAGS, '%s_listen' % name, "0.0.0.0")
port = getattr(FLAGS, '%s_listen_port' % name, 0)
self.server = wsgi.Server(name, self.application, host, port)
""" def _find_config(self):
"""Attempt to find 'paste' configuration file."""
location = wsgi.paste_config_file(self._config_name)
logging.debug(_("Using paste.deploy config at: %s"), location)
return location
def __init__(self, conf, apis): def _load_config(self):
self.conf = conf """Read and return the 'paste' configuration file."""
self.apis = apis return wsgi.load_paste_configuration(self._config_location, self.name)
self.wsgi_app = None
def _load_application(self):
"""Using the loaded configuration, return the WSGI application."""
return wsgi.load_paste_app(self._config_location, self.name)
def start(self): def start(self):
self.wsgi_app = _run_wsgi(self.conf, self.apis) """Start serving this API using loaded configuration."""
self.server.start()
def stop(self):
"""Stop serving this API."""
self.server.stop()
def wait(self): def wait(self):
self.wsgi_app.wait() """Wait for the service to stop serving this API."""
self.server.wait()
def get_socket_info(self, api_name):
"""Returns the (host, port) that an API was started on."""
return self.wsgi_app.socket_info[api_name]
class ApiService(WsgiService):
"""Class for our nova-api service."""
@classmethod
def create(cls, conf=None):
if not conf:
conf = wsgi.paste_config_file(FLAGS.api_paste_config)
if not conf:
message = (_('No paste configuration found for: %s'),
FLAGS.api_paste_config)
raise exception.Error(message)
api_endpoints = ['ec2', 'osapi']
service = cls(conf, api_endpoints)
return service
def serve(*services): def serve(*services):
@@ -321,29 +321,3 @@ def serve_wsgi(cls, conf=None):
service.start() service.start()
return service return service
def _run_wsgi(paste_config_file, apis):
logging.debug(_('Using paste.deploy config at: %s'), paste_config_file)
apps = []
for api in apis:
config = wsgi.load_paste_configuration(paste_config_file, api)
if config is None:
logging.debug(_('No paste configuration for app: %s'), api)
continue
logging.debug(_('App Config: %(api)s\n%(config)r') % locals())
logging.info(_('Running %s API'), api)
app = wsgi.load_paste_app(paste_config_file, api)
apps.append((app,
getattr(FLAGS, '%s_listen_port' % api),
getattr(FLAGS, '%s_listen' % api),
api))
if len(apps) == 0:
logging.error(_('No known API applications configured in %s.'),
paste_config_file)
return
server = wsgi.Server()
for app in apps:
server.start(*app)
return server

View File

@@ -38,7 +38,6 @@ from nova import flags
from nova import rpc from nova import rpc
from nova import utils from nova import utils
from nova import service from nova import service
from nova import wsgi
from nova.virt import fake from nova.virt import fake
@@ -81,7 +80,6 @@ class TestCase(unittest.TestCase):
self.injected = [] self.injected = []
self._services = [] self._services = []
self._monkey_patch_attach() self._monkey_patch_attach()
self._monkey_patch_wsgi()
self._original_flags = FLAGS.FlagValuesDict() self._original_flags = FLAGS.FlagValuesDict()
rpc.ConnectionPool = rpc.Pool(max_size=FLAGS.rpc_conn_pool_size) rpc.ConnectionPool = rpc.Pool(max_size=FLAGS.rpc_conn_pool_size)
@@ -107,7 +105,6 @@ class TestCase(unittest.TestCase):
# Reset our monkey-patches # Reset our monkey-patches
rpc.Consumer.attach_to_eventlet = self.original_attach rpc.Consumer.attach_to_eventlet = self.original_attach
wsgi.Server.start = self.original_start
# Stop any timers # Stop any timers
for x in self.injected: for x in self.injected:
@@ -163,26 +160,6 @@ class TestCase(unittest.TestCase):
_wrapped.func_name = self.original_attach.func_name _wrapped.func_name = self.original_attach.func_name
rpc.Consumer.attach_to_eventlet = _wrapped rpc.Consumer.attach_to_eventlet = _wrapped
def _monkey_patch_wsgi(self):
"""Allow us to kill servers spawned by wsgi.Server."""
self.original_start = wsgi.Server.start
@functools.wraps(self.original_start)
def _wrapped_start(inner_self, *args, **kwargs):
original_spawn_n = inner_self.pool.spawn_n
@functools.wraps(original_spawn_n)
def _wrapped_spawn_n(*args, **kwargs):
rv = greenthread.spawn(*args, **kwargs)
self._services.append(rv)
inner_self.pool.spawn_n = _wrapped_spawn_n
self.original_start(inner_self, *args, **kwargs)
inner_self.pool.spawn_n = original_spawn_n
_wrapped_start.func_name = self.original_start.func_name
wsgi.Server.start = _wrapped_start
# Useful assertions # Useful assertions
def assertDictMatch(self, d1, d2, approx_equal=False, tolerance=0.001): def assertDictMatch(self, d1, d2, approx_equal=False, tolerance=0.001):
"""Assert two dicts are equivalent. """Assert two dicts are equivalent.

View File

@@ -43,46 +43,45 @@ FLAGS = flags.FLAGS
LOG = logging.getLogger('nova.wsgi') LOG = logging.getLogger('nova.wsgi')
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): class Server(object):
"""Server class to manage multiple WSGI sockets and applications.""" """Server class to manage multiple WSGI sockets and applications."""
def __init__(self, threads=1000): default_pool_size = 1000
self.pool = eventlet.GreenPool(threads) logger_name = "eventlet.wsgi.server"
self.socket_info = {}
def start(self, application, port, host='0.0.0.0', key=None, backlog=128): def __init__(self, name, app, host, port, pool_size=None):
"""Run a WSGI server with the given application.""" self.name = name
arg0 = sys.argv[0] self.app = app
logging.audit(_('Starting %(arg0)s on %(host)s:%(port)s') % locals()) self.host = host
socket = eventlet.listen((host, port), backlog=backlog) self.port = port
self.pool.spawn_n(self._run, application, socket) self._pool = eventlet.GreenPool(pool_size or self.default_pool_size)
if key: self._log = logging.WritableLogger(logging.getLogger(self.logger_name))
self.socket_info[key] = socket.getsockname()
def _start(self, socket):
"""Blocking eventlet WSGI server launched from the real 'start'."""
eventlet.wsgi.server(socket,
self.app,
custom_pool=self._pool,
log=self._log)
def start(self, backlog=128):
"""Serve given WSGI application using the given parameters."""
socket = eventlet.listen((self.host, self.port), backlog=backlog)
self._server = eventlet.spawn(self._start, socket)
(self.host, self.port) = socket.getsockname()
LOG.info(_('Starting %(app)s on %(host)s:%(port)s') % self.__dict__)
def stop(self):
"""Stop this server by killing the greenthread running it."""
self._server.kill()
def wait(self): def wait(self):
"""Wait until all servers have completed running.""" """Wait until server has been stopped."""
try: try:
self.pool.waitall() self._server.wait()
except KeyboardInterrupt: except KeyboardInterrupt:
pass 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): class Request(webob.Request):
pass pass
@@ -340,6 +339,8 @@ def paste_config_file(basename):
if os.path.exists(configfile): if os.path.exists(configfile):
return configfile return configfile
raise Exception(_("Unable to find paste.deploy config '%s'") % basename)
def load_paste_configuration(filename, appname): def load_paste_configuration(filename, appname):
"""Returns a paste configuration dict, or None.""" """Returns a paste configuration dict, or None."""