From 91a8e88dc6dbc781cb46e56fe7cfe1e98748f3ce Mon Sep 17 00:00:00 2001 From: Anton Arefiev Date: Fri, 10 Jul 2015 16:44:59 +0300 Subject: [PATCH] Add Cinder API wsgi application This change adds cinder API application, so cinder API can be run under wsgi server(Apache, Nginx, etc). Eventlet will still be used by default. Cinder API with eventlet as a webserver and wsgi application managing has some cons: * Cinder API is deployed in other way as a common web application. Apache/Nginx is generally used web servers for REST API application. * Cinder API is run as a separate service. It means that cloud operators need to configure some software to monitor that the API is running. * Apache/Nginx works better under the real heavy load than eventlet. To use c-api deployment under Apache in devstack assign devstack var CINDER_USE_MOD_WSGI="True". Related changes in devstack: https://review.openstack.org/#/c/204643/ Cinder documentation on how deploy cinder API under Apache: https://review.openstack.org/#/c/207020/ Patch for Infra to make it tested: https://review.openstack.org/#/c/208498/ DocImpact Implements: blueprint: non-eventlet-wsgi-app Change-Id: If877d700b0efaa5406efa8f8f17c5816928e83ce --- cinder/api/middleware/auth.py | 2 +- cinder/api/middleware/fault.py | 2 +- cinder/api/openstack/__init__.py | 2 +- cinder/api/openstack/wsgi.py | 2 +- cinder/api/v1/limits.py | 2 +- cinder/api/v2/limits.py | 2 +- cinder/service.py | 6 +- cinder/tests/unit/api/fakes.py | 2 +- cinder/tests/unit/test_service.py | 10 +- cinder/tests/unit/wsgi/__init__.py | 0 .../test_eventlet_server.py} | 51 ++-- cinder/tests/unit/{api => wsgi}/test_wsgi.py | 2 +- cinder/wsgi/__init__.py | 0 cinder/wsgi/common.py | 284 ++++++++++++++++++ cinder/{wsgi.py => wsgi/eventlet_server.py} | 269 +---------------- cinder/wsgi/wsgi.py | 48 +++ etc/cinder/api-httpd.conf | 16 + 17 files changed, 393 insertions(+), 307 deletions(-) create mode 100644 cinder/tests/unit/wsgi/__init__.py rename cinder/tests/unit/{test_wsgi.py => wsgi/test_eventlet_server.py} (89%) rename cinder/tests/unit/{api => wsgi}/test_wsgi.py (98%) create mode 100644 cinder/wsgi/__init__.py create mode 100644 cinder/wsgi/common.py rename cinder/{wsgi.py => wsgi/eventlet_server.py} (54%) create mode 100644 cinder/wsgi/wsgi.py create mode 100644 etc/cinder/api-httpd.conf diff --git a/cinder/api/middleware/auth.py b/cinder/api/middleware/auth.py index 9789e1b14..110e728a2 100644 --- a/cinder/api/middleware/auth.py +++ b/cinder/api/middleware/auth.py @@ -30,7 +30,7 @@ import webob.exc from cinder.api.openstack import wsgi from cinder import context from cinder.i18n import _ -from cinder import wsgi as base_wsgi +from cinder.wsgi import common as base_wsgi use_forwarded_for_opt = cfg.BoolOpt( diff --git a/cinder/api/middleware/fault.py b/cinder/api/middleware/fault.py index 0575b46f1..ce56d5f90 100644 --- a/cinder/api/middleware/fault.py +++ b/cinder/api/middleware/fault.py @@ -23,7 +23,7 @@ from cinder.api.openstack import wsgi from cinder import exception from cinder.i18n import _, _LE, _LI from cinder import utils -from cinder import wsgi as base_wsgi +from cinder.wsgi import common as base_wsgi LOG = logging.getLogger(__name__) diff --git a/cinder/api/openstack/__init__.py b/cinder/api/openstack/__init__.py index 3b8dc07c2..966a8d0a8 100644 --- a/cinder/api/openstack/__init__.py +++ b/cinder/api/openstack/__init__.py @@ -23,7 +23,7 @@ import routes from cinder.api.openstack import wsgi from cinder.i18n import _, _LW -from cinder import wsgi as base_wsgi +from cinder.wsgi import common as base_wsgi LOG = logging.getLogger(__name__) diff --git a/cinder/api/openstack/wsgi.py b/cinder/api/openstack/wsgi.py index d2502d4eb..78fd20d06 100644 --- a/cinder/api/openstack/wsgi.py +++ b/cinder/api/openstack/wsgi.py @@ -31,7 +31,7 @@ from cinder import exception from cinder import i18n from cinder.i18n import _, _LE, _LI from cinder import utils -from cinder import wsgi +from cinder.wsgi import common as wsgi XML_NS_V1 = 'http://docs.openstack.org/api/openstack-block-storage/1.0/content' diff --git a/cinder/api/v1/limits.py b/cinder/api/v1/limits.py index 16f09a37d..b0124deae 100644 --- a/cinder/api/v1/limits.py +++ b/cinder/api/v1/limits.py @@ -34,7 +34,7 @@ from cinder.api.views import limits as limits_views from cinder.api import xmlutil from cinder.i18n import _ from cinder import quota -from cinder import wsgi as base_wsgi +from cinder.wsgi import common as base_wsgi QUOTAS = quota.QUOTAS LIMITS_PREFIX = "limits." diff --git a/cinder/api/v2/limits.py b/cinder/api/v2/limits.py index b6c21b4da..0b6fb6139 100644 --- a/cinder/api/v2/limits.py +++ b/cinder/api/v2/limits.py @@ -34,7 +34,7 @@ from cinder.api.views import limits as limits_views from cinder.api import xmlutil from cinder.i18n import _ from cinder import quota -from cinder import wsgi as base_wsgi +from cinder.wsgi import common as base_wsgi QUOTAS = quota.QUOTAS LIMITS_PREFIX = "limits." diff --git a/cinder/service.py b/cinder/service.py index 01f929ede..5be0fc1c3 100644 --- a/cinder/service.py +++ b/cinder/service.py @@ -41,8 +41,8 @@ from cinder.i18n import _, _LE, _LI, _LW from cinder.objects import base as objects_base from cinder import rpc from cinder import version -from cinder import wsgi - +from cinder.wsgi import common as wsgi_common +from cinder.wsgi import eventlet_server as wsgi LOG = logging.getLogger(__name__) @@ -355,7 +355,7 @@ class WSGIService(service.ServiceBase): """ self.name = name self.manager = self._get_manager() - self.loader = loader or wsgi.Loader() + self.loader = loader or wsgi_common.Loader() self.app = self.loader.load_app(name) self.host = getattr(CONF, '%s_listen' % name, "0.0.0.0") self.port = getattr(CONF, '%s_listen_port' % name, 0) diff --git a/cinder/tests/unit/api/fakes.py b/cinder/tests/unit/api/fakes.py index a87a0ed82..e567dbc3a 100644 --- a/cinder/tests/unit/api/fakes.py +++ b/cinder/tests/unit/api/fakes.py @@ -29,7 +29,7 @@ from cinder.api.v2 import limits from cinder.api.v2 import router from cinder.api import versions from cinder import context -from cinder import wsgi +from cinder.wsgi import common as wsgi FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' diff --git a/cinder/tests/unit/test_service.py b/cinder/tests/unit/test_service.py index 2d54e7245..2c95d65e5 100644 --- a/cinder/tests/unit/test_service.py +++ b/cinder/tests/unit/test_service.py @@ -31,7 +31,7 @@ from cinder import manager from cinder import rpc from cinder import service from cinder import test -from cinder import wsgi +from cinder.wsgi import common as wsgi test_service_opts = [ @@ -275,25 +275,25 @@ class TestWSGIService(test.TestCase): self.assertEqual(1000, test_service.server._pool.size) self.assertTrue(mock_load_app.called) - @mock.patch('cinder.wsgi.Server') + @mock.patch('cinder.wsgi.eventlet_server.Server') def test_workers_set_default(self, wsgi_server): test_service = service.WSGIService("osapi_volume") self.assertEqual(processutils.get_worker_count(), test_service.workers) - @mock.patch('cinder.wsgi.Server') + @mock.patch('cinder.wsgi.eventlet_server.Server') def test_workers_set_good_user_setting(self, wsgi_server): self.override_config('osapi_volume_workers', 8) test_service = service.WSGIService("osapi_volume") self.assertEqual(8, test_service.workers) - @mock.patch('cinder.wsgi.Server') + @mock.patch('cinder.wsgi.eventlet_server.Server') def test_workers_set_zero_user_setting(self, wsgi_server): self.override_config('osapi_volume_workers', 0) test_service = service.WSGIService("osapi_volume") # If a value less than 1 is used, defaults to number of procs available self.assertEqual(processutils.get_worker_count(), test_service.workers) - @mock.patch('cinder.wsgi.Server') + @mock.patch('cinder.wsgi.eventlet_server.Server') def test_workers_set_negative_user_setting(self, wsgi_server): self.override_config('osapi_volume_workers', -1) self.assertRaises(exception.InvalidInput, diff --git a/cinder/tests/unit/wsgi/__init__.py b/cinder/tests/unit/wsgi/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cinder/tests/unit/test_wsgi.py b/cinder/tests/unit/wsgi/test_eventlet_server.py similarity index 89% rename from cinder/tests/unit/test_wsgi.py rename to cinder/tests/unit/wsgi/test_eventlet_server.py index e60964ab4..c014deb27 100644 --- a/cinder/tests/unit/test_wsgi.py +++ b/cinder/tests/unit/wsgi/test_eventlet_server.py @@ -34,12 +34,13 @@ import webob.dec from cinder import exception from cinder.i18n import _ from cinder import test -import cinder.wsgi +from cinder.wsgi import common as wsgi_common +from cinder.wsgi import eventlet_server as wsgi CONF = cfg.CONF TEST_VAR_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), - 'var')) + '../var')) def open_no_proxy(*args, **kwargs): @@ -67,8 +68,8 @@ class TestLoaderNothingExists(test.TestCase): def test_config_not_found(self): self.assertRaises( - cinder.exception.ConfigNotFound, - cinder.wsgi.Loader, + exception.ConfigNotFound, + wsgi_common.Loader, ) @@ -87,7 +88,7 @@ document_root = /tmp self.config.write(self._paste_config.lstrip()) self.config.seek(0) self.config.flush() - self.loader = cinder.wsgi.Loader(self.config.name) + self.loader = wsgi_common.Loader(self.config.name) self.addCleanup(self.config.close) def test_config_found(self): @@ -95,7 +96,7 @@ document_root = /tmp def test_app_not_found(self): self.assertRaises( - cinder.exception.PasteAppNotFound, + exception.PasteAppNotFound, self.loader.load_app, "non-existent app", ) @@ -115,12 +116,12 @@ class TestWSGIServer(test.TestCase): return False def test_no_app(self): - server = cinder.wsgi.Server("test_app", None, - host="127.0.0.1", port=0) + server = wsgi.Server("test_app", None, + host="127.0.0.1", port=0) self.assertEqual("test_app", server.name) def test_start_random_port(self): - server = cinder.wsgi.Server("test_random_port", None, host="127.0.0.1") + server = wsgi.Server("test_random_port", None, host="127.0.0.1") server.start() self.assertNotEqual(0, server.port) server.stop() @@ -129,9 +130,9 @@ class TestWSGIServer(test.TestCase): @testtools.skipIf(not _ipv6_configured(), "Test requires an IPV6 configured interface") def test_start_random_port_with_ipv6(self): - server = cinder.wsgi.Server("test_random_port", - None, - host="::1") + server = wsgi.Server("test_random_port", + None, + host="::1") server.start() self.assertEqual("::1", server.host) self.assertNotEqual(0, server.port) @@ -140,8 +141,8 @@ class TestWSGIServer(test.TestCase): def test_server_pool_waitall(self): # test pools waitall method gets called while stopping server - server = cinder.wsgi.Server("test_server", None, - host="127.0.0.1") + server = wsgi.Server("test_server", None, + host="127.0.0.1") server.start() with mock.patch.object(server._pool, 'waitall') as mock_waitall: @@ -160,8 +161,8 @@ class TestWSGIServer(test.TestCase): start_response('200 OK', [('Content-Type', 'text/plain')]) return [greetings] - server = cinder.wsgi.Server("test_app", hello_world, - host="127.0.0.1", port=0) + server = wsgi.Server("test_app", hello_world, + host="127.0.0.1", port=0) server.start() response = open_no_proxy('http://127.0.0.1:%d/' % server.port) @@ -176,8 +177,8 @@ class TestWSGIServer(test.TestCase): start_response('200 OK', [('Content-Type', 'text/plain')]) return [greetings] - server = cinder.wsgi.Server("test_app", hello_world, - host="127.0.0.1", port=0) + server = wsgi.Server("test_app", hello_world, + host="127.0.0.1", port=0) server.start() s = socket.socket() @@ -215,8 +216,8 @@ class TestWSGIServer(test.TestCase): def hello_world(req): return greetings - server = cinder.wsgi.Server("test_app", hello_world, - host="127.0.0.1", port=0) + server = wsgi.Server("test_app", hello_world, + host="127.0.0.1", port=0) server.start() @@ -239,10 +240,10 @@ class TestWSGIServer(test.TestCase): def hello_world(req): return greetings - server = cinder.wsgi.Server("test_app", - hello_world, - host="::1", - port=0) + server = wsgi.Server("test_app", + hello_world, + host="::1", + port=0) server.start() response = open_no_proxy('https://[::1]:%d/' % server.port) @@ -251,7 +252,7 @@ class TestWSGIServer(test.TestCase): server.stop() def test_reset_pool_size_to_default(self): - server = cinder.wsgi.Server("test_resize", None, host="127.0.0.1") + server = wsgi.Server("test_resize", None, host="127.0.0.1") server.start() # Stopping the server, which in turn sets pool size to 0 diff --git a/cinder/tests/unit/api/test_wsgi.py b/cinder/tests/unit/wsgi/test_wsgi.py similarity index 98% rename from cinder/tests/unit/api/test_wsgi.py rename to cinder/tests/unit/wsgi/test_wsgi.py index 2eafc544d..63f055016 100644 --- a/cinder/tests/unit/api/test_wsgi.py +++ b/cinder/tests/unit/wsgi/test_wsgi.py @@ -27,7 +27,7 @@ from cinder import test import routes import webob -from cinder import wsgi +from cinder.wsgi import common as wsgi class Test(test.TestCase): diff --git a/cinder/wsgi/__init__.py b/cinder/wsgi/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cinder/wsgi/common.py b/cinder/wsgi/common.py new file mode 100644 index 000000000..2fd4e6abe --- /dev/null +++ b/cinder/wsgi/common.py @@ -0,0 +1,284 @@ +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Utility methods for working with WSGI servers.""" + +import sys + +from oslo_config import cfg +from oslo_log import log as logging +from paste import deploy +import routes.middleware +import webob.dec +import webob.exc + +from cinder import exception +from cinder.i18n import _, _LE +from cinder import utils + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +class Request(webob.Request): + pass + + +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 = cinder.api.fancy_api:Wadl.factory + + which would result in a call to the `Wadl` class as + + import cinder.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) + 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 = cinder.api.analytics:Analytics.factory + + which would result in a call to the `Analytics` class as + + import cinder.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') # noqa + for key, value in req.environ.items(): + print(key, '=', value) # noqa + print() # noqa + resp = req.get_response(self.application) + + print(('*' * 40) + ' RESPONSE HEADERS') # noqa + for (key, value) in resp.headers.items(): + print(key, '=', value) # noqa + print() # noqa + + 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') # noqa + for part in app_iter: + sys.stdout.write(part) + sys.stdout.flush() + yield part + print() # noqa + + +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 + + """ + config_path = config_path or CONF.api_paste_config + self.config_path = utils.find_config(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: `cinder.exception.PasteAppNotFound` + + """ + try: + return deploy.loadapp("config:%s" % self.config_path, name=name) + except LookupError: + LOG.exception(_LE("Error loading app %s"), name) + raise exception.PasteAppNotFound(name=name, path=self.config_path) diff --git a/cinder/wsgi.py b/cinder/wsgi/eventlet_server.py similarity index 54% rename from cinder/wsgi.py rename to cinder/wsgi/eventlet_server.py index 6b8512277..f40595680 100644 --- a/cinder/wsgi.py +++ b/cinder/wsgi/eventlet_server.py @@ -1,8 +1,3 @@ -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# Copyright 2010 OpenStack Foundation -# All Rights Reserved. -# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at @@ -15,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -"""Utility methods for working with WSGI servers.""" +"""Methods for working with eventlet WSGI servers.""" from __future__ import print_function @@ -23,7 +18,6 @@ import errno import os import socket import ssl -import sys import time import eventlet @@ -34,14 +28,10 @@ from oslo_log import log as logging from oslo_service import service from oslo_utils import excutils from oslo_utils import netutils -from paste import deploy -import routes.middleware -import webob.dec -import webob.exc + from cinder import exception from cinder.i18n import _, _LE, _LI -from cinder import utils socket_opts = [ @@ -231,7 +221,7 @@ class Server(service.ServiceBase): **ssl_kwargs) except Exception: with excutils.save_and_reraise_exception(): - LOG.error(_LE("Failed to start %(name)s on %(_host)s:" + LOG.error(_LE("Failed to start %(name)s on %(_host)s: " "%(_port)s with SSL " "support."), self.__dict__) @@ -293,256 +283,3 @@ class Server(service.ServiceBase): """ self._pool.resize(self.pool_size) - - -class Request(webob.Request): - pass - - -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 = cinder.api.fancy_api:Wadl.factory - - which would result in a call to the `Wadl` class as - - import cinder.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) - 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 = cinder.api.analytics:Analytics.factory - - which would result in a call to the `Analytics` class as - - import cinder.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') # noqa - for key, value in req.environ.items(): - print(key, '=', value) # noqa - print() # noqa - resp = req.get_response(self.application) - - print(('*' * 40) + ' RESPONSE HEADERS') # noqa - for (key, value) in resp.headers.items(): - print(key, '=', value) # noqa - print() # noqa - - 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') # noqa - for part in app_iter: - sys.stdout.write(part) - sys.stdout.flush() - yield part - print() # noqa - - -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 - - """ - config_path = config_path or CONF.api_paste_config - self.config_path = utils.find_config(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: `cinder.exception.PasteAppNotFound` - - """ - try: - return deploy.loadapp("config:%s" % self.config_path, name=name) - except LookupError: - LOG.exception(_LE("Error loading app %s"), name) - raise exception.PasteAppNotFound(name=name, path=self.config_path) diff --git a/cinder/wsgi/wsgi.py b/cinder/wsgi/wsgi.py new file mode 100644 index 000000000..d51bec40d --- /dev/null +++ b/cinder/wsgi/wsgi.py @@ -0,0 +1,48 @@ +# 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. + +"""Cinder OS API WSGI application.""" + + +import sys +import warnings + +from cinder import objects + +warnings.simplefilter('once', DeprecationWarning) + +from oslo_config import cfg +from oslo_log import log as logging + +from cinder import i18n +i18n.enable_lazy() + +# Need to register global_opts +from cinder.common import config # noqa +from cinder import rpc +from cinder import version +from cinder.wsgi import common as wsgi_common + +CONF = cfg.CONF + + +def _application(): + objects.register_all() + CONF(sys.argv[1:], project='cinder', + version=version.version_string()) + logging.setup(CONF, "cinder") + + rpc.init(CONF) + return wsgi_common.Loader().load_app(name='osapi_volume') + + +application = _application() diff --git a/etc/cinder/api-httpd.conf b/etc/cinder/api-httpd.conf new file mode 100644 index 000000000..f3555477a --- /dev/null +++ b/etc/cinder/api-httpd.conf @@ -0,0 +1,16 @@ +Listen 8776 +LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\" %D(us)" cinder_combined + + + WSGIDaemonProcess osapi_volume processes=2 threads=1 user=cinder display-name=%{GROUP} + WSGIProcessGroup osapi_volume + WSGIScriptAlias / /var/www/cgi-bin/cinder/osapi_volume + WSGIApplicationGroup %{GLOBAL} + WSGIPassAuthorization On + = 2.4> + ErrorLogFormat "%{cu}t %M" + + ErrorLog /var/log/apache2/cinder_error.log + CustomLog /var/log/apache2/cinder.log cinder_combined + + -- 2.45.2