From ef6f4e4c8ec82e2c9f9988fe2e04591ee01220e6 Mon Sep 17 00:00:00 2001 From: Chris Dent Date: Mon, 15 Jan 2018 20:58:48 +0000 Subject: [PATCH] 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 --- nova/api/auth.py | 2 +- nova/api/metadata/handler.py | 2 +- nova/api/openstack/__init__.py | 18 +- nova/api/openstack/auth.py | 2 +- nova/api/openstack/compute/routes.py | 2 +- nova/api/openstack/requestlog.py | 2 +- nova/api/openstack/wsgi.py | 39 ++- nova/api/wsgi.py | 298 ++++++++++++++++++ nova/service.py | 3 +- nova/tests/fixtures.py | 2 +- .../api/openstack/compute/test_versions.py | 2 +- nova/tests/unit/api/openstack/fakes.py | 2 +- nova/tests/unit/api/test_wsgi.py | 2 +- nova/tests/unit/test_service.py | 2 +- nova/tests/unit/test_wsgi.py | 7 +- nova/utils.py | 13 - nova/wsgi.py | 274 ---------------- 17 files changed, 355 insertions(+), 317 deletions(-) create mode 100644 nova/api/wsgi.py diff --git a/nova/api/auth.py b/nova/api/auth.py index c59b2d502d88..4663d6444a99 100644 --- a/nova/api/auth.py +++ b/nova/api/auth.py @@ -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 diff --git a/nova/api/metadata/handler.py b/nova/api/metadata/handler.py index 64053218724a..eb3e84e2e29a 100644 --- a/nova/api/metadata/handler.py +++ b/nova/api/metadata/handler.py @@ -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__) diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 3443554015ff..13467c180236 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -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)() diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index a3bcd0cad156..d5966b8ce6a4 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -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 diff --git a/nova/api/openstack/compute/routes.py b/nova/api/openstack/compute/routes.py index a9f209c5be7f..e6d55c27347c 100644 --- a/nova/api/openstack/compute/routes.py +++ b/nova/api/openstack/compute/routes.py @@ -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 diff --git a/nova/api/openstack/requestlog.py b/nova/api/openstack/requestlog.py index 85c0e8ed5725..3c41f114f0a5 100644 --- a/nova/api/openstack/requestlog.py +++ b/nova/api/openstack/requestlog.py @@ -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__) diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py index 3f7eebc2372a..2eef33c01265 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -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] = \ diff --git a/nova/api/wsgi.py b/nova/api/wsgi.py new file mode 100644 index 000000000000..a365d1320d65 --- /dev/null +++ b/nova/api/wsgi.py @@ -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) diff --git a/nova/service.py b/nova/service.py index a04a2e96d559..5f43d8557a65 100644 --- a/nova/service.py +++ b/nova/service.py @@ -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'): diff --git a/nova/tests/fixtures.py b/nova/tests/fixtures.py index 0871e077e482..fbc9be16f9f4 100644 --- a/nova/tests/fixtures.py +++ b/nova/tests/fixtures.py @@ -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') diff --git a/nova/tests/unit/api/openstack/compute/test_versions.py b/nova/tests/unit/api/openstack/compute/test_versions.py index 98e5568b5826..8603f7e0eccd 100644 --- a/nova/tests/unit/api/openstack/compute/test_versions.py +++ b/nova/tests/unit/api/openstack/compute/test_versions.py @@ -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 = { diff --git a/nova/tests/unit/api/openstack/fakes.py b/nova/tests/unit/api/openstack/fakes.py index 554c40b54dc5..0d58501d301f 100644 --- a/nova/tests/unit/api/openstack/fakes.py +++ b/nova/tests/unit/api/openstack/fakes.py @@ -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 diff --git a/nova/tests/unit/api/test_wsgi.py b/nova/tests/unit/api/test_wsgi.py index d83cc67fbd40..4ad4055752b3 100644 --- a/nova/tests/unit/api/test_wsgi.py +++ b/nova/tests/unit/api/test_wsgi.py @@ -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): diff --git a/nova/tests/unit/test_service.py b/nova/tests/unit/test_service.py index 5b83d5496aad..bbba577f2472 100644 --- a/nova/tests/unit/test_service.py +++ b/nova/tests/unit/test_service.py @@ -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') diff --git a/nova/tests/unit/test_wsgi.py b/nova/tests/unit/test_wsgi.py index c5ecbf7cb751..4b863d015e5c 100644 --- a/nova/tests/unit/test_wsgi.py +++ b/nova/tests/unit/test_wsgi.py @@ -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) diff --git a/nova/utils.py b/nova/utils.py index 9d835fcd7222..8abe8db07121 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -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. diff --git a/nova/wsgi.py b/nova/wsgi.py index c841b2060b56..07021f16c3fd 100644 --- a/nova/wsgi.py +++ b/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)