Refactor WSGI apps and utils to limit imports
The file nova/api/openstack/__init__.py had imported a lot of modules, notably nova.utils. This means that any code which runs within that package, notably the placement service, imports all those modules, even if it is not going to use them. This results in scripts/binaries that are heavier than they need to be and in some cases including modules, like eventlet, that it would feel safe to not have in the stack. Unfortunately we cannot sinply rename nova/api/openstack/__init__.py to another name because it contains FaultWrapper and FaultWrapper is referred to, by package path, from the paste.ini file and that file is out there in config land, and something we prefer not to change. Therefore alternate methods of cleaning up were explored and this has led to some useful changes: Fault wrapper is the only consumer of walk_class_hierarchy so there is no reason for it it to be in nova.utils. nova.wsgi contains a mismash of WSGI middleware and applications, which need only a small number of imports, and Server classes which are more complex and not required by the WSGI wares. Therefore nova.wsgi was split into nova.wsgi and nova.api.wsgi. The name choices may not be ideal, but they were chosen to limit the cascades of changes that are needed across code and tests. Where utils.utf8 was used it has been replaced with the similar (but not exactly equivalient) method from oslo_utils.encodeutils. Change-Id: I297f30aa6eb01fe3b53fd8c9b7853949be31156d Partial-Bug: #1743120
This commit is contained in:
parent
5f881ff030
commit
ef6f4e4c8e
@ -22,10 +22,10 @@ from oslo_serialization import jsonutils
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
|
||||
from nova.api import wsgi
|
||||
import nova.conf
|
||||
from nova import context
|
||||
from nova.i18n import _
|
||||
from nova import wsgi
|
||||
|
||||
|
||||
CONF = nova.conf.CONF
|
||||
|
@ -27,13 +27,13 @@ import webob.dec
|
||||
import webob.exc
|
||||
|
||||
from nova.api.metadata import base
|
||||
from nova.api import wsgi
|
||||
from nova import cache_utils
|
||||
import nova.conf
|
||||
from nova import context as nova_context
|
||||
from nova import exception
|
||||
from nova.i18n import _
|
||||
from nova.network.neutronv2 import api as neutronapi
|
||||
from nova import wsgi
|
||||
|
||||
CONF = nova.conf.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
@ -24,16 +24,28 @@ import webob.dec
|
||||
import webob.exc
|
||||
|
||||
from nova.api.openstack import wsgi
|
||||
from nova.api import wsgi as base_wsgi
|
||||
import nova.conf
|
||||
from nova.i18n import translate
|
||||
from nova import utils
|
||||
from nova import wsgi as base_wsgi
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = nova.conf.CONF
|
||||
|
||||
|
||||
def walk_class_hierarchy(clazz, encountered=None):
|
||||
"""Walk class hierarchy, yielding most derived classes first."""
|
||||
if not encountered:
|
||||
encountered = []
|
||||
for subclass in clazz.__subclasses__():
|
||||
if subclass not in encountered:
|
||||
encountered.append(subclass)
|
||||
# drill down to leaves first
|
||||
for subsubclass in walk_class_hierarchy(subclass, encountered):
|
||||
yield subsubclass
|
||||
yield subclass
|
||||
|
||||
|
||||
class FaultWrapper(base_wsgi.Middleware):
|
||||
"""Calls down the middleware stack, making exceptions into faults."""
|
||||
|
||||
@ -42,7 +54,7 @@ class FaultWrapper(base_wsgi.Middleware):
|
||||
@staticmethod
|
||||
def status_to_type(status):
|
||||
if not FaultWrapper._status_to_type:
|
||||
for clazz in utils.walk_class_hierarchy(webob.exc.HTTPError):
|
||||
for clazz in walk_class_hierarchy(webob.exc.HTTPError):
|
||||
FaultWrapper._status_to_type[clazz.code] = clazz
|
||||
return FaultWrapper._status_to_type.get(
|
||||
status, webob.exc.HTTPInternalServerError)()
|
||||
|
@ -18,9 +18,9 @@ import webob.dec
|
||||
import webob.exc
|
||||
|
||||
from nova.api.openstack import wsgi
|
||||
from nova.api import wsgi as base_wsgi
|
||||
import nova.conf
|
||||
from nova import context
|
||||
from nova import wsgi as base_wsgi
|
||||
|
||||
CONF = nova.conf.CONF
|
||||
|
||||
|
@ -93,8 +93,8 @@ from nova.api.openstack.compute import versionsV21
|
||||
from nova.api.openstack.compute import virtual_interfaces
|
||||
from nova.api.openstack.compute import volumes
|
||||
from nova.api.openstack import wsgi
|
||||
from nova.api import wsgi as base_wsgi
|
||||
import nova.conf
|
||||
from nova import wsgi as base_wsgi
|
||||
|
||||
|
||||
CONF = nova.conf.CONF
|
||||
|
@ -20,7 +20,7 @@ import webob.dec
|
||||
import webob.exc
|
||||
|
||||
from nova.api.openstack import wsgi
|
||||
from nova import wsgi as base_wsgi
|
||||
from nova.api import wsgi as base_wsgi
|
||||
|
||||
# TODO(sdague) maybe we can use a better name here for the logger
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
@ -26,11 +26,10 @@ import webob
|
||||
|
||||
from nova.api.openstack import api_version_request as api_version
|
||||
from nova.api.openstack import versioned_method
|
||||
from nova.api import wsgi
|
||||
from nova import exception
|
||||
from nova import i18n
|
||||
from nova.i18n import _
|
||||
from nova import utils
|
||||
from nova import wsgi
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@ -334,16 +333,28 @@ class ResponseObject(object):
|
||||
if self.obj is not None:
|
||||
body = serializer.serialize(self.obj)
|
||||
response = webob.Response(body=body)
|
||||
if response.headers.get('Content-Length'):
|
||||
# NOTE(andreykurilin): we need to encode 'Content-Length' header,
|
||||
# since webob.Response auto sets it if "body" attr is presented.
|
||||
# https://github.com/Pylons/webob/blob/1.5.0b0/webob/response.py#L147
|
||||
response.headers['Content-Length'] = utils.utf8(
|
||||
response.headers['Content-Length'])
|
||||
response.status_int = self.code
|
||||
for hdr, value in self._headers.items():
|
||||
response.headers[hdr] = utils.utf8(value)
|
||||
response.headers['Content-Type'] = utils.utf8(content_type)
|
||||
for hdr, val in self._headers.items():
|
||||
if not isinstance(val, six.text_type):
|
||||
val = six.text_type(val)
|
||||
if six.PY2:
|
||||
# In Py2.X Headers must be byte strings
|
||||
response.headers[hdr] = encodeutils.safe_encode(val)
|
||||
else:
|
||||
# In Py3.X Headers must be utf-8 strings
|
||||
response.headers[hdr] = encodeutils.safe_decode(
|
||||
encodeutils.safe_encode(val))
|
||||
# Deal with content_type
|
||||
if not isinstance(content_type, six.text_type):
|
||||
content_type = six.text_type(content_type)
|
||||
if six.PY2:
|
||||
# In Py2.X Headers must be byte strings
|
||||
response.headers['Content-Type'] = encodeutils.safe_encode(
|
||||
content_type)
|
||||
else:
|
||||
# In Py3.X Headers must be utf-8 strings
|
||||
response.headers['Content-Type'] = encodeutils.safe_decode(
|
||||
encodeutils.safe_encode(content_type))
|
||||
return response
|
||||
|
||||
@property
|
||||
@ -658,13 +669,15 @@ class Resource(wsgi.Application):
|
||||
|
||||
if hasattr(response, 'headers'):
|
||||
for hdr, val in list(response.headers.items()):
|
||||
if not isinstance(val, six.text_type):
|
||||
val = six.text_type(val)
|
||||
if six.PY2:
|
||||
# In Py2.X Headers must be byte strings
|
||||
response.headers[hdr] = utils.utf8(val)
|
||||
response.headers[hdr] = encodeutils.safe_encode(val)
|
||||
else:
|
||||
# In Py3.X Headers must be utf-8 strings
|
||||
response.headers[hdr] = encodeutils.safe_decode(
|
||||
utils.utf8(val))
|
||||
encodeutils.safe_encode(val))
|
||||
|
||||
if not request.api_version_request.is_null():
|
||||
response.headers[API_VERSION_REQUEST_HEADER] = \
|
||||
|
298
nova/api/wsgi.py
Normal file
298
nova/api/wsgi.py
Normal file
@ -0,0 +1,298 @@
|
||||
# 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.
|
||||
"""WSGI primitives used throughout the nova WSGI apps."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from oslo_log import log as logging
|
||||
from paste import deploy
|
||||
import routes.middleware
|
||||
import six
|
||||
import webob
|
||||
|
||||
import nova.conf
|
||||
from nova import exception
|
||||
from nova.i18n import _, _LE
|
||||
|
||||
|
||||
CONF = nova.conf.CONF
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Request(webob.Request):
|
||||
def __init__(self, environ, *args, **kwargs):
|
||||
if CONF.wsgi.secure_proxy_ssl_header:
|
||||
scheme = environ.get(CONF.wsgi.secure_proxy_ssl_header)
|
||||
if scheme:
|
||||
environ['wsgi.url_scheme'] = scheme
|
||||
super(Request, self).__init__(environ, *args, **kwargs)
|
||||
|
||||
|
||||
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(explanation='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 ...)
|
||||
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.items():
|
||||
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(six.text_type(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
|
||||
|
||||
"""
|
||||
self.config_path = None
|
||||
|
||||
config_path = config_path or CONF.wsgi.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 exception.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: `nova.exception.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 exception.PasteAppNotFound(name=name, path=self.config_path)
|
@ -27,6 +27,7 @@ import oslo_messaging as messaging
|
||||
from oslo_service import service
|
||||
from oslo_utils import importutils
|
||||
|
||||
from nova.api import wsgi as api_wsgi
|
||||
from nova import baserpc
|
||||
from nova import conductor
|
||||
import nova.conf
|
||||
@ -326,7 +327,7 @@ class WSGIService(service.Service):
|
||||
self.binary = 'nova-%s' % name
|
||||
self.topic = None
|
||||
self.manager = self._get_manager()
|
||||
self.loader = loader or wsgi.Loader()
|
||||
self.loader = loader or api_wsgi.Loader()
|
||||
self.app = self.loader.load_app(name)
|
||||
# inherit all compute_api worker counts from osapi_compute
|
||||
if name.startswith('openstack_compute_api'):
|
||||
|
@ -39,6 +39,7 @@ from wsgi_intercept import interceptor
|
||||
from nova.api.openstack.compute import tenant_networks
|
||||
from nova.api.openstack.placement import deploy as placement_deploy
|
||||
from nova.api.openstack import wsgi_app
|
||||
from nova.api import wsgi
|
||||
from nova.compute import rpcapi as compute_rpcapi
|
||||
from nova import context
|
||||
from nova.db import migration
|
||||
@ -53,7 +54,6 @@ from nova import rpc
|
||||
from nova import service
|
||||
from nova.tests.functional.api import client
|
||||
from nova.tests import uuidsentinel
|
||||
from nova import wsgi
|
||||
|
||||
_TRUE_VALUES = ('True', 'true', '1', 'yes')
|
||||
|
||||
|
@ -19,11 +19,11 @@ from oslo_serialization import jsonutils
|
||||
|
||||
from nova.api.openstack import api_version_request as avr
|
||||
from nova.api.openstack.compute import views
|
||||
from nova.api import wsgi
|
||||
from nova import test
|
||||
from nova.tests.unit.api.openstack import fakes
|
||||
from nova.tests.unit import matchers
|
||||
from nova.tests import uuidsentinel as uuids
|
||||
from nova import wsgi
|
||||
|
||||
|
||||
NS = {
|
||||
|
@ -30,6 +30,7 @@ from nova.api.openstack import compute
|
||||
from nova.api.openstack.compute import versions
|
||||
from nova.api.openstack import urlmap
|
||||
from nova.api.openstack import wsgi as os_wsgi
|
||||
from nova.api import wsgi
|
||||
from nova.compute import flavors
|
||||
from nova.compute import vm_states
|
||||
import nova.conf
|
||||
@ -44,7 +45,6 @@ from nova.tests.unit import fake_block_device
|
||||
from nova.tests.unit import fake_network
|
||||
from nova.tests.unit.objects import test_keypair
|
||||
from nova import utils
|
||||
from nova import wsgi
|
||||
|
||||
|
||||
CONF = nova.conf.CONF
|
||||
|
@ -24,8 +24,8 @@ import routes
|
||||
from six.moves import StringIO
|
||||
import webob
|
||||
|
||||
from nova.api import wsgi
|
||||
from nova import test
|
||||
from nova import wsgi
|
||||
|
||||
|
||||
class Test(test.NoDBTestCase):
|
||||
|
@ -263,7 +263,7 @@ class TestWSGIService(test.NoDBTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestWSGIService, self).setUp()
|
||||
self.stub_out('nova.wsgi.Loader.load_app',
|
||||
self.stub_out('nova.api.wsgi.Loader.load_app',
|
||||
lambda *a, **kw: mock.MagicMock())
|
||||
|
||||
@mock.patch('nova.objects.Service.get_by_host_and_binary')
|
||||
|
@ -29,6 +29,7 @@ import six
|
||||
import testtools
|
||||
import webob
|
||||
|
||||
import nova.api.wsgi
|
||||
import nova.exception
|
||||
from nova import test
|
||||
from nova.tests.unit import utils
|
||||
@ -51,14 +52,14 @@ class TestLoaderNothingExists(test.NoDBTestCase):
|
||||
self.flags(api_paste_config='api-paste.ini', group='wsgi')
|
||||
self.assertRaises(
|
||||
nova.exception.ConfigNotFound,
|
||||
nova.wsgi.Loader,
|
||||
nova.api.wsgi.Loader,
|
||||
)
|
||||
|
||||
def test_asbpath_config_not_found(self):
|
||||
self.flags(api_paste_config='/etc/nova/api-paste.ini', group='wsgi')
|
||||
self.assertRaises(
|
||||
nova.exception.ConfigNotFound,
|
||||
nova.wsgi.Loader,
|
||||
nova.api.wsgi.Loader,
|
||||
)
|
||||
|
||||
|
||||
@ -77,7 +78,7 @@ document_root = /tmp
|
||||
self.config.write(self._paste_config.lstrip())
|
||||
self.config.seek(0)
|
||||
self.config.flush()
|
||||
self.loader = nova.wsgi.Loader(self.config.name)
|
||||
self.loader = nova.api.wsgi.Loader(self.config.name)
|
||||
|
||||
def test_config_found(self):
|
||||
self.assertEqual(self.config.name, self.loader.config_path)
|
||||
|
@ -683,19 +683,6 @@ def tempdir(**kwargs):
|
||||
LOG.error(_LE('Could not remove tmpdir: %s'), e)
|
||||
|
||||
|
||||
def walk_class_hierarchy(clazz, encountered=None):
|
||||
"""Walk class hierarchy, yielding most derived classes first."""
|
||||
if not encountered:
|
||||
encountered = []
|
||||
for subclass in clazz.__subclasses__():
|
||||
if subclass not in encountered:
|
||||
encountered.append(subclass)
|
||||
# drill down to leaves first
|
||||
for subsubclass in walk_class_hierarchy(subclass, encountered):
|
||||
yield subsubclass
|
||||
yield subclass
|
||||
|
||||
|
||||
class UndoManager(object):
|
||||
"""Provides a mechanism to facilitate rolling back a series of actions
|
||||
when an exception is raised.
|
||||
|
274
nova/wsgi.py
274
nova/wsgi.py
@ -22,7 +22,6 @@ from __future__ import print_function
|
||||
import os.path
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
|
||||
import eventlet
|
||||
import eventlet.wsgi
|
||||
@ -30,11 +29,6 @@ import greenlet
|
||||
from oslo_log import log as logging
|
||||
from oslo_service import service
|
||||
from oslo_utils import excutils
|
||||
from paste import deploy
|
||||
import routes.middleware
|
||||
import six
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
|
||||
import nova.conf
|
||||
from nova import exception
|
||||
@ -230,271 +224,3 @@ class Server(service.ServiceBase):
|
||||
self._server.wait()
|
||||
except greenlet.GreenletExit:
|
||||
LOG.info(_LI("WSGI server has stopped."))
|
||||
|
||||
|
||||
class Request(webob.Request):
|
||||
def __init__(self, environ, *args, **kwargs):
|
||||
if CONF.wsgi.secure_proxy_ssl_header:
|
||||
scheme = environ.get(CONF.wsgi.secure_proxy_ssl_header)
|
||||
if scheme:
|
||||
environ['wsgi.url_scheme'] = scheme
|
||||
super(Request, self).__init__(environ, *args, **kwargs)
|
||||
|
||||
|
||||
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(explanation='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 ...)
|
||||
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.items():
|
||||
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(six.text_type(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
|
||||
|
||||
"""
|
||||
self.config_path = None
|
||||
|
||||
config_path = config_path or CONF.wsgi.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 exception.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: `nova.exception.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 exception.PasteAppNotFound(name=name, path=self.config_path)
|
||||
|
Loading…
Reference in New Issue
Block a user