Merge "Refactor WSGI apps and utils to limit imports"
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user